Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-08-26 13:18:39 +02:00
commit 1378c1a779
372 changed files with 26005 additions and 20082 deletions

View file

@ -85,13 +85,12 @@
feedback?.setData(undefined)
return
}
feedback?.setData(validator?.getFeedback(v, getCountry))
if (!validator?.isValid(v, getCountry)) {
feedback?.setData(validator?.getFeedback(v, getCountry))
value.setData(undefined)
return
}
feedback?.setData(undefined)
if (selectedUnit.data) {
value.setData(unit.toOsm(v, selectedUnit.data))
} else {

View file

@ -1,7 +1,25 @@
import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
export default class UrlValidator extends Validator {
private readonly _forceHttps: boolean
private static readonly spamWebsites = new Set<string>([
"booking.com",
"hotel-details-guide.com",
"tripingguide.com",
"tripadvisor.com",
"tripadvisor.co.uk",
"tripadvisor.com.au",
"katestravelexperience.eu",
"hoteldetails.eu"
])
private static readonly discouragedWebsites = new Set<string>([
"facebook.com"
])
constructor(name?: string, explanation?: string, forceHttps?: boolean) {
super(
name ?? "url",
@ -11,6 +29,11 @@ export default class UrlValidator extends Validator {
)
this._forceHttps = forceHttps ?? false
}
/**
*
* new UrlValidator().reformat("https://example.com/page?fbclid=123456&utm_source=mastodon") // => "https://example.com/page"
*/
reformat(str: string): string {
try {
let url: URL
@ -63,7 +86,52 @@ export default class UrlValidator extends Validator {
}
}
/**
*
* const v = new UrlValidator()
* v.getFeedback("example.").textFor("en") // => "This is not a valid web address"
* v.getFeedback("https://booking.com/some-hotel.html").textFor("en") // => Translations.t.validation.url.spamSite.Subs({host: "booking.com"}).textFor("en")
*/
getFeedback(s: string, getCountry?: () => string): Translation | undefined {
if (
!s.startsWith("http://") &&
!s.startsWith("https://") &&
!s.startsWith("http:")
) {
s = "https://" + s
}
try{
const url = new URL(s)
let host = url.host.toLowerCase()
if (host.startsWith("www.")) {
host = host.slice(4)
}
if (UrlValidator.spamWebsites.has(host)) {
return Translations.t.validation.url.spamSite.Subs({ host })
}
if (UrlValidator.discouragedWebsites.has(host)) {
return Translations.t.validation.url.aggregator.Subs({ host })
}
}catch (e) {
// pass
}
const upstream = super.getFeedback(s, getCountry)
if (upstream) {
return upstream
}
return undefined
}
/**
* const v = new UrlValidator()
* v.isValid("https://booking.com/some-hotel.html") // => false
*/
isValid(str: string): boolean {
try {
if (
!str.startsWith("http://") &&
@ -73,6 +141,15 @@ export default class UrlValidator extends Validator {
str = "https://" + str
}
const url = new URL(str)
let host = url.host.toLowerCase()
if (host.startsWith("www.")) {
host = host.slice(4)
}
if (UrlValidator.spamWebsites.has(host)) {
return false
}
const dotIndex = url.host.indexOf(".")
return dotIndex > 0 && url.host[url.host.length - 1] !== "."
} catch (e) {

View file

@ -43,6 +43,8 @@
import Train from "../../assets/svg/Train.svelte"
import Airport from "../../assets/svg/Airport.svelte"
import BuildingStorefront from "@babeard/svelte-heroicons/outline/BuildingStorefront"
import LockClosed from "@babeard/svelte-heroicons/solid/LockClosed"
import Key from "@babeard/svelte-heroicons/solid/Key"
/**
* Renders a single icon.
@ -54,7 +56,7 @@
export let color: string | undefined = undefined
export let clss: string | undefined = ""
clss ??= ""
export let emojiHeight = 40
export let emojiHeight = "40px"
</script>
{#if icon}
@ -150,6 +152,10 @@
<PencilIcon class={clss} {color} />
{:else if icon === "user_circle"}
<UserCircleIcon class={clss} {color} />
{:else if icon === "lock"}
<LockClosed class={clss} {color} />
{:else if icon === "key"}
<Key class={clss} {color} />
{:else if icon==="globe_alt"}
<GlobeAltIcon class={clss} {color} />
{:else if icon === "building_office_2"}
@ -163,10 +169,9 @@
{:else if icon === "building_storefront"}
<BuildingStorefront {color} class={clss}/>
{:else if Utils.isEmoji(icon)}
<span style={`font-size: ${emojiHeight}px; line-height: ${emojiHeight}px`}>
<span style={`font-size: ${emojiHeight}; line-height: ${emojiHeight}`}>
{icon}
</span>
{:else}
<img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true" alt="" />
{/if}

View file

@ -30,6 +30,7 @@
* Class which is applied onto the individual icons
*/
export let clss = ""
export let emojiHeight: string = "40px"
/**
* Class applied onto the entire element
@ -41,7 +42,7 @@
<div class={twMerge("relative", size)}>
{#each iconsParsed as icon}
<div class="absolute top-0 left-0 flex h-full w-full items-center">
<Icon icon={icon.icon} color={icon.color} {clss} />
<Icon icon={icon.icon} color={icon.color} {clss} {emojiHeight} />
</div>
{/each}
</div>

View file

@ -93,6 +93,15 @@
{#each availableLayers as availableLayer}
<option value={availableLayer.properties.id}>
{availableLayer.properties.name}
{#if availableLayer.properties.category.startsWith("historic")}
⏱️
{/if}
{#if availableLayer.properties.category.endsWith("elevation")}
{/if}
{#if availableLayer.properties.best}
{/if}
</option>
{/each}
</select>

View file

@ -53,8 +53,6 @@ export class ShareLinkViz implements SpecialVisualization {
}
}
return new SvelteUIElement(ShareButton, { generateShareData, text }).SetClass(
"w-full h-full"
)
return new SvelteUIElement(ShareButton, { generateShareData, text })
}
}

View file

@ -7,6 +7,14 @@
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { twJoin } from "tailwind-merge"
import Marker from "../../Map/Marker.svelte"
import ToSvelte from "../../Base/ToSvelte.svelte"
import { And } from "../../../Logic/Tags/And"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import BaseUIElement from "../../BaseUIElement"
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig"
import SvelteUIElement from "../../Base/SvelteUIElement"
import Icon from "../../Map/Icon.svelte"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
export let selectedElement: Feature
export let tags: UIEventSource<Record<string, string>>
@ -31,20 +39,44 @@
| "large-height"
| string
}
const emojiHeights = {
small: "2rem",
medium: "3rem",
large: "5rem",
}
function getAutoIcon(mapping: { if?: TagsFilter }): BaseUIElement {
for (const preset of layer.presets) {
if (!new And(preset.tags).shadows(mapping.if)) {
continue
}
return layer.defaultIcon(TagUtils.asProperties(preset.tags))
}
return undefined
}
</script>
{#if mapping.icon !== undefined && !noIcons}
<div class="inline-flex items-center">
<Marker
icons={mapping.icon}
size={twJoin(
`mapping-icon-${mapping.iconClass ?? "small"}-height mapping-icon-${
mapping.iconClass ?? "small"
}-width`,
"shrink-0"
)}
clss={`mapping-icon-${mapping.iconClass ?? "small"}`}
/>
{#if mapping.icon === "auto"}
<div class="mr-2 h-8 w-8 shrink-0">
<ToSvelte construct={() => getAutoIcon(mapping)} />
</div>
{:else}
<Marker
icons={mapping.icon}
size={twJoin(
"shrink-0",
`mapping-icon-${mapping.iconClass ?? "small"}-height mapping-icon-${
mapping.iconClass ?? "small"
}-width`
)}
emojiHeight={emojiHeights[mapping.iconClass] ?? "2rem"}
clss={`mapping-icon-${mapping.iconClass ?? "small"}`}
/>
{/if}
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement} {clss} />
</div>
{:else if mapping.then !== undefined}

View file

@ -46,7 +46,9 @@ export default class SpecialVisualisationUtils {
}
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
const matched = template.match(new RegExp(`(.*?){\([a-zA-Z_]+\)\\((.*?)\\)(:.*)?}(.*)`, "s"))
const matched = template.match(
new RegExp(`(.*?){\([a-zA-Z_]+\)\\((.*?)\\)(:.*)?}(.*)`, "s")
)
if (matched === null) {
// IF we end up here, no changes have to be made - except to remove any resting {}
return [template]

View file

@ -2031,41 +2031,6 @@ export default class SpecialVisualizations {
return new VariableUiElement(translation)
},
},
{
funcName: "preset_type_select",
docs: "An editable tag rendering which allows to change the type",
args: [],
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
selectedElement: Feature,
layer: LayerConfig
): SvelteUIElement {
const t = Translations.t.preset_type
const question: QuestionableTagRenderingConfigJson = {
id: layer.id + "-type",
question: t.question.translations,
mappings: layer.presets.map((pr) => {
return {
if: new And(pr.tags).asJson(),
then: (pr.description ? t.typeDescription : t.typeTitle).Subs({
title: pr.title,
description: pr.description,
}).translations,
}
}),
}
const config = new TagRenderingConfig(question)
return new SvelteUIElement(TagRenderingEditable, {
config,
tags,
selectedElement,
state,
layer,
})
},
},
{
funcName: "pending_changes",
docs: "A module showing the pending changes, with the option to clear the pending changes",
@ -2149,15 +2114,14 @@ export default class SpecialVisualizations {
const question: QuestionableTagRenderingConfigJson = {
id: layer.id + "-type",
question: t.question.translations,
mappings: layer.presets.map((pr) => {
return {
if: new And(pr.tags).asJson(),
then: (pr.description ? t.typeDescription : t.typeTitle).Subs({
title: pr.title,
description: pr.description,
}).translations,
}
}),
mappings: layer.presets.map((pr) => ({
if: new And(pr.tags).asJson(),
icon: "auto",
then: (pr.description ? t.typeDescription : t.typeTitle).Subs({
title: pr.title,
description: pr.description,
}).translations,
})),
}
const config = new TagRenderingConfig(question)
return new SvelteUIElement(TagRenderingEditable, {
@ -2169,74 +2133,6 @@ export default class SpecialVisualizations {
})
},
},
{
funcName: "pending_changes",
docs: "A module showing the pending changes, with the option to clear the pending changes",
args: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
return new SvelteUIElement(PendingChangesIndicator, { state, compact: false })
},
},
{
funcName: "clear_caches",
docs: "A button which clears the locally downloaded data and the service worker. Login status etc will be kept",
args: [
{
name: "text",
required: true,
doc: "The text to show on the button",
},
],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
argument: string[],
feature: Feature,
layer: LayerConfig
): SvelteUIElement {
return new SvelteUIElement<any, any, any>(ClearCaches, {
msg: argument[0] ?? "Clear local caches",
})
},
},
{
funcName: "group",
docs: "A collapsable group (accordion)",
args: [
{
name: "header",
doc: "The _identifier_ of a single tagRendering. This will be used as header",
},
{
name: "labels",
doc: "A `;`-separated list of either identifiers or label names. All tagRenderings matching this value will be shown in the accordion",
},
],
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
argument: string[],
selectedElement: Feature,
layer: LayerConfig
): SvelteUIElement {
const [header, labelsStr] = argument
const labels = labelsStr.split(";").map((x) => x.trim())
return new SvelteUIElement<any, any, any>(GroupedView, {
state,
tags,
selectedElement,
layer,
header,
labels,
})
},
},
]
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
@ -2245,6 +2141,7 @@ export default class SpecialVisualizations {
const invalid = specialVisualizations
.map((sp, i) => ({ sp, i }))
.filter((sp) => sp.sp.funcName === undefined || !sp.sp.funcName.match(regex))
if (invalid.length > 0) {
throw (
"Invalid special visualisation found: funcName is undefined or doesn't match " +
@ -2254,6 +2151,16 @@ export default class SpecialVisualizations {
)
}
const allNames = specialVisualizations.map((f) => f.funcName)
const seen = new Set<string>()
for (let name of allNames) {
name = name.toLowerCase()
if (seen.has(name)) {
throw "Invalid special visualisations: detected a duplicate name: " + name
}
seen.add(name)
}
return specialVisualizations
}
}