forked from MapComplete/MapComplete
		
	Merge develop
This commit is contained in:
		
						commit
						1378c1a779
					
				
					 372 changed files with 26005 additions and 20082 deletions
				
			
		|  | @ -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 { | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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 }) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
|  | @ -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] | ||||
|  |  | |||
|  | @ -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 | ||||
|     } | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue