forked from MapComplete/MapComplete
		
	More refactoring
This commit is contained in:
		
							parent
							
								
									5d0fe31c41
								
							
						
					
					
						commit
						41e6a2c760
					
				
					 147 changed files with 1540 additions and 1797 deletions
				
			
		|  | @ -2,7 +2,6 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig" | |||
| import { Utils } from "../Utils" | ||||
| import known_themes from "../assets/generated/known_layers.json" | ||||
| import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" | ||||
| import { ALL } from "dns" | ||||
| import { AllKnownLayouts } from "./AllKnownLayouts" | ||||
| export class AllSharedLayers { | ||||
|     public static sharedLayers: Map<string, LayerConfig> = AllSharedLayers.getSharedLayers() | ||||
|  |  | |||
|  | @ -1,12 +1,13 @@ | |||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer" | ||||
| import Combine from "../../UI/Base/Combine" | ||||
| import { Utils } from "../../Utils" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { Feature } from "geojson" | ||||
| import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import SvelteUIElement from "../../UI/Base/SvelteUIElement" | ||||
| import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer.svelte" | ||||
| 
 | ||||
| export default class TitleHandler { | ||||
|     constructor( | ||||
|  | @ -32,7 +33,7 @@ export default class TitleHandler { | |||
|                         const tagsSource = | ||||
|                             allElements.getStore(tags.id) ?? | ||||
|                             new UIEventSource<Record<string, string>>(tags) | ||||
|                         const title = new TagRenderingAnswer(tagsSource, layer.title, {}) | ||||
|                         const title = new SvelteUIElement(TagRenderingAnswer, { tags: tagsSource }) | ||||
|                         return ( | ||||
|                             new Combine([defaultTitle, " | ", title]).ConstructElement() | ||||
|                                 ?.textContent ?? defaultTitle | ||||
|  |  | |||
|  | @ -1,12 +1,4 @@ | |||
| import FeatureSource, { Tiled } from "../FeatureSource" | ||||
| import { Tiles } from "../../../Models/TileRange" | ||||
| import { IdbLocalStorage } from "../../Web/IdbLocalStorage" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
| import { BBox } from "../../BBox" | ||||
| import SimpleFeatureSource from "../Sources/SimpleFeatureSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import Loc from "../../../Models/Loc" | ||||
| import FeatureSource from "../FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| import TileLocalStorage from "./TileLocalStorage" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { FeatureSourceForLayer, Tiled } from "../FeatureSource" | ||||
| import { BBox } from "../../BBox" | ||||
| import { FeatureSourceForLayer } from "../FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| 
 | ||||
| export default class SimpleFeatureSource implements FeatureSourceForLayer { | ||||
|  |  | |||
							
								
								
									
										52
									
								
								Logic/FeatureSource/Sources/SnappingFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								Logic/FeatureSource/Sources/SnappingFeatureSource.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | |||
| import FeatureSource from "../FeatureSource" | ||||
| import { Store } from "../../UIEventSource" | ||||
| import { Feature, Point } from "geojson" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| 
 | ||||
| export interface SnappingOptions { | ||||
|     /** | ||||
|      * If the distance is bigger then this amount, don't snap. | ||||
|      * In meter | ||||
|      */ | ||||
|     maxDistance?: number | ||||
| } | ||||
| 
 | ||||
| export default class SnappingFeatureSource implements FeatureSource { | ||||
|     public readonly features: Store<Feature<Point>[]> | ||||
| 
 | ||||
|     constructor( | ||||
|         snapTo: FeatureSource, | ||||
|         location: Store<{ lon: number; lat: number }>, | ||||
|         options?: SnappingOptions | ||||
|     ) { | ||||
|         const simplifiedFeatures = snapTo.features.mapD((features) => | ||||
|             features | ||||
|                 .filter((feature) => feature.geometry.type !== "Point") | ||||
|                 .map((f) => GeoOperations.forceLineString(<any>f)) | ||||
|         ) | ||||
| 
 | ||||
|         location.mapD( | ||||
|             ({ lon, lat }) => { | ||||
|                 const features = snapTo.features.data | ||||
|                 const loc: [number, number] = [lon, lat] | ||||
|                 const maxDistance = (options?.maxDistance ?? 1000) * 1000 | ||||
|                 let bestSnap: Feature<Point, { "snapped-to": string; dist: number }> = undefined | ||||
|                 for (const feature of features) { | ||||
|                     const snapped = GeoOperations.nearestPoint(<any>feature, loc) | ||||
|                     if (snapped.properties.dist > maxDistance) { | ||||
|                         continue | ||||
|                     } | ||||
|                     if ( | ||||
|                         bestSnap === undefined || | ||||
|                         bestSnap.properties.dist > snapped.properties.dist | ||||
|                     ) { | ||||
|                         snapped.properties["snapped-to"] = feature.properties.id | ||||
|                         bestSnap = <any>snapped | ||||
|                     } | ||||
|                 } | ||||
|                 return bestSnap | ||||
|             }, | ||||
|             [snapTo.features] | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -2,7 +2,6 @@ import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource" | |||
| import StaticFeatureSource from "./StaticFeatureSource" | ||||
| import { GeoOperations } from "../../GeoOperations" | ||||
| import { BBox } from "../../BBox" | ||||
| import exp from "constants" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import { | |||
|     GeoJSON, | ||||
|     Geometry, | ||||
|     LineString, | ||||
|     MultiLineString, | ||||
|     MultiPolygon, | ||||
|     Point, | ||||
|     Polygon, | ||||
|  | @ -272,17 +273,42 @@ export class GeoOperations { | |||
|      * @param point Point defined as [lon, lat] | ||||
|      */ | ||||
|     public static nearestPoint( | ||||
|         way: Feature<LineString | Polygon>, | ||||
|         way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>, | ||||
|         point: [number, number] | ||||
|     ): Feature<Point> { | ||||
|     ): Feature< | ||||
|         Point, | ||||
|         { | ||||
|             index: number | ||||
|             dist: number | ||||
|             location: number | ||||
|         } | ||||
|     > { | ||||
|         return <any>( | ||||
|             turf.nearestPointOnLine(<Feature<LineString>>way, point, { units: "kilometers" }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Helper method to reuse the coordinates of the way as LineString. | ||||
|      * Mostly used as helper for 'nearestPoint' | ||||
|      * @param way | ||||
|      */ | ||||
|     public static forceLineString( | ||||
|         way: Feature<LineString | MultiLineString | Polygon | MultiPolygon> | ||||
|     ): Feature<LineString | MultiLineString> { | ||||
|         if (way.geometry.type === "Polygon") { | ||||
|             way = { ...way } | ||||
|             way.geometry = { ...way.geometry } | ||||
|             way.geometry.type = "LineString" | ||||
|             way.geometry.coordinates = (<Polygon>way.geometry).coordinates[0] | ||||
|         } else if (way.geometry.type === "MultiPolygon") { | ||||
|             way = { ...way } | ||||
|             way.geometry = { ...way.geometry } | ||||
|             way.geometry.type = "MultiLineString" | ||||
|             way.geometry.coordinates = (<MultiPolygon>way.geometry).coordinates[0] | ||||
|         } | ||||
| 
 | ||||
|         return turf.nearestPointOnLine(<Feature<LineString>>way, point, { units: "kilometers" }) | ||||
|         return <any>way | ||||
|     } | ||||
| 
 | ||||
|     public static toCSV(features: any[]): string { | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import GenericImageProvider from "./GenericImageProvider" | |||
| import { Store, UIEventSource } from "../UIEventSource" | ||||
| import ImageProvider, { ProvidedImage } from "./ImageProvider" | ||||
| import { WikidataImageProvider } from "./WikidataImageProvider" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| 
 | ||||
| /** | ||||
|  * A generic 'from the interwebz' image picker, without attribution | ||||
|  | @ -44,7 +45,7 @@ export default class AllImageProviders { | |||
|         UIEventSource<ProvidedImage[]> | ||||
|     >() | ||||
| 
 | ||||
|     public static LoadImagesFor(tags: Store<any>, tagKey?: string[]): Store<ProvidedImage[]> { | ||||
|     public static LoadImagesFor(tags: Store<OsmTags>, tagKey?: string[]): Store<ProvidedImage[]> { | ||||
|         if (tags.data.id === undefined) { | ||||
|             return undefined | ||||
|         } | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ export default class ChangeLocationAction extends OsmChangeAction { | |||
|         this._meta = meta | ||||
|     } | ||||
| 
 | ||||
|     protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
|     protected async CreateChangeDescriptions(): Promise<ChangeDescription[]> { | ||||
|         const d: ChangeDescription = { | ||||
|             changes: { | ||||
|                 lat: this._newLonLat[1], | ||||
|  |  | |||
|  | @ -71,7 +71,7 @@ export default class ChangeTagAction extends OsmChangeAction { | |||
|         return { k: key.trim(), v: value.trim() } | ||||
|     } | ||||
| 
 | ||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
|     async CreateChangeDescriptions(): Promise<ChangeDescription[]> { | ||||
|         const changedTags: { k: string; v: string }[] = this._tagsFilter | ||||
|             .asChange(this._currentTags) | ||||
|             .map(ChangeTagAction.checkChange) | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ import { OsmConnection } from "../Osm/OsmConnection" | |||
| import { MangroveIdentity } from "../Web/MangroveReviews" | ||||
| import { Store, Stores, UIEventSource } from "../UIEventSource" | ||||
| import Locale from "../../UI/i18n/Locale" | ||||
| import { Changes } from "../Osm/Changes" | ||||
| import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource" | ||||
| import FeatureSource from "../FeatureSource/FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
|  |  | |||
|  | @ -122,7 +122,7 @@ export class Tag extends TagsFilter { | |||
|         return [this] | ||||
|     } | ||||
| 
 | ||||
|     asChange(properties: any): { k: string; v: string }[] { | ||||
|     asChange(): { k: string; v: string }[] { | ||||
|         return [{ k: this.key, v: this.value }] | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Store, UIEventSource } from "../Logic/UIEventSource" | ||||
| import { UIEventSource } from "../Logic/UIEventSource" | ||||
| import { BBox } from "../Logic/BBox" | ||||
| import { RasterLayerPolygon } from "./RasterLayers" | ||||
| 
 | ||||
|  |  | |||
|  | @ -26,7 +26,6 @@ import Table from "../../UI/Base/Table" | |||
| import FilterConfigJson from "./Json/FilterConfigJson" | ||||
| import { And } from "../../Logic/Tags/And" | ||||
| import { Overpass } from "../../Logic/Osm/Overpass" | ||||
| import Constants from "../Constants" | ||||
| import { FixedUiElement } from "../../UI/Base/FixedUiElement" | ||||
| import Svg from "../../Svg" | ||||
| import { ImmutableStore } from "../../Logic/UIEventSource" | ||||
|  |  | |||
|  | @ -108,7 +108,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|         this.availableLayers = AvailableRasterLayers.layersAvailableAt(this.mapProperties.location) | ||||
| 
 | ||||
|         this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) | ||||
|         const indexedElements = new LayoutSource( | ||||
|         this.indexedFeatures = new LayoutSource( | ||||
|             layout.layers, | ||||
|             this.featureSwitches, | ||||
|             new StaticFeatureSource([]), | ||||
|  | @ -116,6 +116,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | |||
|             this.osmConnection.Backend(), | ||||
|             (id) => this.layerState.filteredLayers.get(id).isDisplayed | ||||
|         ) | ||||
|         const indexedElements = this.indexedFeatures | ||||
|         this.featureProperties = new FeaturePropertiesStore(indexedElements) | ||||
|         const perLayer = new PerLayerFeatureSourceSplitter( | ||||
|             Array.from(this.layerState.filteredLayers.values()), | ||||
|  |  | |||
|  | @ -15,7 +15,6 @@ import { OsmConnection } from "../Logic/Osm/OsmConnection" | |||
| export default class AllThemesGui { | ||||
|     setup() { | ||||
|         try { | ||||
|             new FixedUiElement("").AttachTo("centermessage") | ||||
|             const osmConnection = new OsmConnection() | ||||
|             const state = new UserRelatedState(osmConnection) | ||||
|             const intro = new Combine([ | ||||
|  | @ -38,15 +37,14 @@ export default class AllThemesGui { | |||
|                 new FixedUiElement("v" + Constants.vNumber), | ||||
|             ]) | ||||
|                 .SetClass("block m-5 lg:w-3/4 lg:ml-40") | ||||
|                 .SetStyle("pointer-events: all;") | ||||
|                 .AttachTo("top-left") | ||||
|                 .AttachTo("main") | ||||
|         } catch (e) { | ||||
|             console.error(">>>> CRITICAL", e) | ||||
|             new FixedUiElement( | ||||
|                 "Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!" | ||||
|             ) | ||||
|                 .SetClass("alert") | ||||
|                 .AttachTo("centermessage") | ||||
|                 .AttachTo("main") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import BaseUIElement from "../BaseUIElement" | ||||
| import { VariableUiElement } from "./VariableUIElement" | ||||
| import { Stores, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { Stores } from "../../Logic/UIEventSource" | ||||
| import Loading from "./Loading" | ||||
| 
 | ||||
| export default class AsyncLazy extends BaseUIElement { | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| import { UIElement } from "../UIElement" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
							
								
								
									
										89
									
								
								UI/Base/DragInvitation.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								UI/Base/DragInvitation.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,89 @@ | |||
| <script lang="ts"> | ||||
|   /** | ||||
|    * This overlay element will regularly show a hand that swipes over the underlying element. | ||||
|    * This element will hide as soon as the Store 'hideSignal' receives a change (which is not undefined) | ||||
|    */ | ||||
|   import ToSvelte from "./ToSvelte.svelte"; | ||||
|   import Svg from "../../Svg"; | ||||
|   import { Store } from "../../Logic/UIEventSource"; | ||||
|   import { onDestroy } from "svelte"; | ||||
| 
 | ||||
|   let mainElem: HTMLElement; | ||||
|   export let hideSignal: Store<any>; | ||||
|   function hide(){ | ||||
|     console.trace("Hiding...") | ||||
|       mainElem.style.visibility = "hidden"; | ||||
|   } | ||||
|   if (hideSignal) { | ||||
|     onDestroy(hideSignal.addCallbackD(() => { | ||||
|       console.trace("Hiding invitation") | ||||
|       return true; | ||||
|     })); | ||||
|   } | ||||
|    | ||||
| $: { | ||||
|     console.log("Binding listeners on", mainElem) | ||||
|   mainElem?.addEventListener("click",_ => hide()) | ||||
|   mainElem?.addEventListener("touchstart",_ => hide()) | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| 
 | ||||
| <div bind:this={mainElem} class="absolute bottom-0 right-0 w-full h-full"> | ||||
|   <div id="hand-container"> | ||||
|     <ToSvelte construct={Svg.hand_ui}></ToSvelte> | ||||
|   </div> | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
| 
 | ||||
|     @keyframes hand-drag-animation { | ||||
|         /* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */ | ||||
|         0% { | ||||
|             opacity: 0; | ||||
|             transform: rotate(-30deg); | ||||
|         } | ||||
| 
 | ||||
|         6% { | ||||
|             opacity: 1; | ||||
|             transform: rotate(-30deg); | ||||
|         } | ||||
| 
 | ||||
|         12% { | ||||
|             opacity: 1; | ||||
|             transform: rotate(-45deg); | ||||
|         } | ||||
| 
 | ||||
|         24% { | ||||
|             opacity: 1; | ||||
|             transform: rotate(-00deg); | ||||
|         } | ||||
| 
 | ||||
|         30% { | ||||
|             opacity: 1; | ||||
|             transform: rotate(-30deg); | ||||
|         } | ||||
| 
 | ||||
|         36% { | ||||
|             opacity: 0; | ||||
|             transform: rotate(-30deg); | ||||
|         } | ||||
| 
 | ||||
|         100% { | ||||
|             opacity: 0; | ||||
|             transform: rotate(-30deg); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     #hand-container { | ||||
|       position: absolute; | ||||
|       width: 2rem; | ||||
|       left: calc(50% + 4rem);  | ||||
|       top: calc(50%);  | ||||
|       opacity: 0.7; | ||||
|       animation: hand-drag-animation 4s ease-in-out infinite; | ||||
|       transform-origin: 50% 125%; | ||||
|   } | ||||
|    | ||||
| </style> | ||||
							
								
								
									
										19
									
								
								UI/Base/FromHtml.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								UI/Base/FromHtml.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| <script lang="ts"> | ||||
|   /** | ||||
|    * Given an HTML string, properly shows this | ||||
|    */ | ||||
| 
 | ||||
|   export let src: string; | ||||
|   let htmlElem: HTMLElement; | ||||
|   $: { | ||||
|     if(htmlElem !== undefined){ | ||||
|     htmlElem.innerHTML = src | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| {#if src !== undefined} | ||||
|   <span bind:this={htmlElem}></span> | ||||
| {/if} | ||||
| 
 | ||||
|  | @ -1,6 +1,6 @@ | |||
| import Translations from "../i18n/Translations" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { Store } from "../../Logic/UIEventSource" | ||||
| 
 | ||||
| export default class Link extends BaseUIElement { | ||||
|     private readonly _href: string | Store<string> | ||||
|  |  | |||
							
								
								
									
										30
									
								
								UI/Base/Tr.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								UI/Base/Tr.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| <script lang="ts"> | ||||
|   /** | ||||
|    * Properly renders a translation | ||||
|    */ | ||||
|   import { Translation } from "../i18n/Translation"; | ||||
|   import { onDestroy } from "svelte"; | ||||
|   import Locale from "../i18n/Locale"; | ||||
|   import { Utils } from "../../Utils"; | ||||
|   import FromHtml from "./FromHtml.svelte"; | ||||
| 
 | ||||
|   export let t: Translation; | ||||
|   export let tags: Record<string, string> | undefined; | ||||
|   // Text for the current language | ||||
|   let txt: string | undefined; | ||||
| 
 | ||||
|   onDestroy(Locale.language.addCallbackAndRunD(l => { | ||||
|     const translation = t?.textFor(l) | ||||
|     if(translation === undefined){ | ||||
|       return | ||||
|     } | ||||
|     if(tags){ | ||||
|       txt = Utils.SubstituteKeys(txt, tags) | ||||
|     }else{ | ||||
|       txt = translation | ||||
|     } | ||||
|   })); | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <FromHtml src={txt}></FromHtml> | ||||
|  | @ -13,7 +13,6 @@ import { OpenIdEditor, OpenJosm } from "./CopyrightPanel" | |||
| import Toggle from "../Input/Toggle" | ||||
| import ScrollableFullScreen from "../Base/ScrollableFullScreen" | ||||
| import { DefaultGuiState } from "../DefaultGuiState" | ||||
| import DefaultGUI from "../DefaultGUI" | ||||
| 
 | ||||
| export class BackToThemeOverview extends Toggle { | ||||
|     constructor( | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | |||
| import { BBox } from "../../Logic/BBox" | ||||
| import FilteredLayer, { FilterState } from "../../Models/FilteredLayer" | ||||
| import geojson2svg from "geojson2svg" | ||||
| import Constants from "../../Models/Constants" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| 
 | ||||
| export class DownloadPanel extends Toggle { | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ import UserRelatedState from "../../Logic/State/UserRelatedState" | |||
| import Loc from "../../Models/Loc" | ||||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import CopyrightPanel from "./CopyrightPanel" | ||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" | ||||
| import PrivacyPolicy from "./PrivacyPolicy" | ||||
| import Hotkeys from "../Base/Hotkeys" | ||||
|  |  | |||
|  | @ -15,8 +15,6 @@ | |||
| 
 | ||||
|   Translations.t; | ||||
|   export let bounds: UIEventSource<BBox> | ||||
|   export let layout: LayoutConfig; | ||||
|   export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | ||||
|   export let selectedElement: UIEventSource<Feature>; | ||||
|   export let selectedLayer: UIEventSource<LayerConfig>; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,23 +1,15 @@ | |||
| <script lang="ts"> | ||||
|   import type { Feature } from "geojson"; | ||||
|   import { Store, UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import TagRenderingAnswer from "../Popup/TagRenderingAnswer"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte"; | ||||
|   import { VariableUiElement } from "../Base/VariableUIElement.js"; | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
|   import { onDestroy } from "svelte"; | ||||
|   import TagRenderingAnswer from "../Popup/TagRenderingAnswer.svelte"; | ||||
| 
 | ||||
|   export let selectedElement: UIEventSource<Feature>; | ||||
|   export let layer: UIEventSource<LayerConfig>; | ||||
|   export let tags: Store<UIEventSource<Record<string, string>>>; | ||||
|   let _tags: UIEventSource<Record<string, string>>; | ||||
|   onDestroy(tags.subscribe(tags => { | ||||
|     _tags = tags; | ||||
|     return false | ||||
|   })); | ||||
|   export let selectedElement: Feature; | ||||
|   export let layer: LayerConfig; | ||||
|   export let tags: UIEventSource<Record<string, string>>; | ||||
| 
 | ||||
|   export let specialVisState: SpecialVisualizationState; | ||||
|   export let state: SpecialVisualizationState; | ||||
| 
 | ||||
|   /** | ||||
|    *        const title = new TagRenderingAnswer( | ||||
|  | @ -46,30 +38,27 @@ | |||
| </script> | ||||
| 
 | ||||
| <div> | ||||
|   <div on:click={() =>selectedElement.setData(undefined)}>close</div> | ||||
|   <div class="flex flex-col sm:flex-row flex-grow justify-between"> | ||||
|     <!-- Title element--> | ||||
|     <ToSvelte | ||||
|       construct={() => new VariableUiElement(tags.mapD(tags =>   new TagRenderingAnswer(tags, layer.data.title, specialVisState), [layer]))}></ToSvelte> | ||||
|     <h3> | ||||
|       <TagRenderingAnswer config={layer.title} {tags} {selectedElement}></TagRenderingAnswer> | ||||
|     </h3> | ||||
| 
 | ||||
|     <div class="flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2"> | ||||
| 
 | ||||
|       {#each $layer.titleIcons as titleIconConfig (titleIconConfig.id)} | ||||
|       {#each layer.titleIcons as titleIconConfig (titleIconConfig.id)} | ||||
|         <div class="w-8 h-8"> | ||||
|           <ToSvelte | ||||
|             construct={() => new VariableUiElement(tags.mapD(tags =>   new TagRenderingAnswer(tags, titleIconConfig, specialVisState)))}></ToSvelte> | ||||
|           <TagRenderingAnswer config={titleIconConfig} {tags} {selectedElement}></TagRenderingAnswer> | ||||
|         </div> | ||||
| 
 | ||||
|       {/each} | ||||
|     </div> | ||||
| 
 | ||||
| 
 | ||||
|   </div> | ||||
| 
 | ||||
|   <ul> | ||||
| 
 | ||||
|     {#each Object.keys($_tags) as key} | ||||
|       <li><b>{key}</b>=<b>{$_tags[key]}</b></li> | ||||
|   <div class="flex flex-col"> | ||||
|     {#each layer.tagRenderings as config (config.id)} | ||||
|       <TagRenderingAnswer {tags} {config} {state}></TagRenderingAnswer> | ||||
|     {/each} | ||||
|   </ul> | ||||
|   </div> | ||||
| 
 | ||||
| </div> | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ | |||
|   import Constants from "../../Models/Constants" | ||||
|   import type Loc from "../../Models/Loc" | ||||
|   import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"; | ||||
|   import Tr from "../Base/Tr.svelte"; | ||||
| 
 | ||||
|   export let theme: LayoutInformation | ||||
|   export let isCustom: boolean = false | ||||
|  | @ -16,8 +17,8 @@ | |||
|   $: title = new Translation( | ||||
|     theme.title, | ||||
|     !isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined | ||||
|   ).toString() | ||||
|   $: description = new Translation(theme.shortDescription).toString() | ||||
|   ) | ||||
|   $: description = new Translation(theme.shortDescription) | ||||
| 
 | ||||
|   // TODO: Improve this function | ||||
|   function createUrl( | ||||
|  | @ -83,8 +84,10 @@ | |||
|       <img slot="image" src={theme.icon} class="block h-11 w-11 bg-red mx-4" alt="" /> | ||||
|       <span slot="message" class="message"> | ||||
|         <span> | ||||
|           <span>{title}</span> | ||||
|           <span>{description}</span> | ||||
|           <Tr t={title}></Tr> | ||||
|           <span class="subtle"> | ||||
|             <Tr t={description}></Tr> | ||||
|           </span> | ||||
|         </span> | ||||
|       </span> | ||||
|     </SubtleButton> | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs" | |||
| import MapControlButton from "./MapControlButton" | ||||
| import Svg from "../Svg" | ||||
| import Toggle from "./Input/Toggle" | ||||
| import SearchAndGo from "./BigComponents/SearchAndGo" | ||||
| import BaseUIElement from "./BaseUIElement" | ||||
| import LeftControls from "./BigComponents/LeftControls" | ||||
| import RightControls from "./BigComponents/RightControls" | ||||
|  | @ -26,7 +25,6 @@ import UserInformationPanel from "./BigComponents/UserInformation" | |||
| import { LoginToggle } from "./Popup/LoginButton" | ||||
| import { FixedUiElement } from "./Base/FixedUiElement" | ||||
| import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" | ||||
| import { GeoLocationState } from "../Logic/State/GeoLocationState" | ||||
| import Hotkeys from "./Base/Hotkeys" | ||||
| import CopyrightPanel from "./BigComponents/CopyrightPanel" | ||||
| import SvelteUIElement from "./Base/SvelteUIElement" | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { Store } from "../../Logic/UIEventSource" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| import { Utils } from "../../Utils" | ||||
| import Combine from "../Base/Combine" | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ import Minimap from "../Base/Minimap" | |||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" | ||||
| import Loc from "../../Models/Loc" | ||||
| import Attribution from "../BigComponents/Attribution" | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" | ||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | ||||
| import ValidatedTextField from "../Input/ValidatedTextField" | ||||
|  |  | |||
|  | @ -7,7 +7,6 @@ import Translations from "../i18n/Translations" | |||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import Toggle from "../Input/Toggle" | ||||
| import { UIElement } from "../UIElement" | ||||
| import { FixedUiElement } from "../Base/FixedUiElement" | ||||
| 
 | ||||
| export interface FlowStep<T> extends BaseUIElement { | ||||
|     readonly IsValid: Store<boolean> | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ import { BBox } from "../../Logic/BBox" | |||
| import UserRelatedState from "../../Logic/State/UserRelatedState" | ||||
| import Translations from "../i18n/Translations" | ||||
| import { AllKnownLayouts } from "../../Customizations/AllKnownLayouts" | ||||
| import Constants from "../../Models/Constants" | ||||
| import { DropDown } from "../Input/DropDown" | ||||
| import { Utils } from "../../Utils" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|  |  | |||
|  | @ -1,118 +0,0 @@ | |||
| import { InputElement } from "./InputElement" | ||||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Combine from "../Base/Combine" | ||||
| import Svg from "../../Svg" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| import { FixedUiElement } from "../Base/FixedUiElement" | ||||
| import { Utils } from "../../Utils" | ||||
| import Loc from "../../Models/Loc" | ||||
| import Minimap from "../Base/Minimap" | ||||
| 
 | ||||
| /** | ||||
|  * Selects a direction in degrees | ||||
|  */ | ||||
| export default class DirectionInput extends InputElement<string> { | ||||
|     public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|     private readonly _location: UIEventSource<Loc> | ||||
|     private readonly value: UIEventSource<string> | ||||
|     private background | ||||
| 
 | ||||
|     constructor( | ||||
|         mapBackground: UIEventSource<any>, | ||||
|         location: UIEventSource<Loc>, | ||||
|         value?: UIEventSource<string> | ||||
|     ) { | ||||
|         super() | ||||
|         this._location = location | ||||
|         this.value = value ?? new UIEventSource<string>(undefined) | ||||
|         this.background = mapBackground | ||||
|     } | ||||
| 
 | ||||
|     GetValue(): UIEventSource<string> { | ||||
|         return this.value | ||||
|     } | ||||
| 
 | ||||
|     IsValid(str: string): boolean { | ||||
|         const t = Number(str) | ||||
|         return !isNaN(t) && t >= 0 && t <= 360 | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         let map: BaseUIElement = new FixedUiElement("") | ||||
|         if (!Utils.runningFromConsole) { | ||||
|             map = Minimap.createMiniMap({ | ||||
|                 background: this.background, | ||||
|                 allowMoving: false, | ||||
|                 location: this._location, | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         const element = new Combine([ | ||||
|             Svg.direction_stroke_svg() | ||||
|                 .SetStyle( | ||||
|                     `position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${ | ||||
|                         this.value.data ?? 0 | ||||
|                     }deg);` | ||||
|                 ) | ||||
|                 .SetClass("direction-svg relative") | ||||
|                 .SetStyle("z-index: 1000"), | ||||
|             map.SetStyle(`position: absolute;top: 0;left: 0;width: 100%;height: 100%;`), | ||||
|         ]) | ||||
|             .SetStyle("width: min(100%, 25em); height: 0; padding-bottom: 100%") // A bit a weird CSS   , see https://stackoverflow.com/questions/13851940/pure-css-solution-square-elements#19448481
 | ||||
|             .SetClass("relative block bg-white border border-black overflow-hidden rounded-full") | ||||
|             .ConstructElement() | ||||
| 
 | ||||
|         this.value.addCallbackAndRunD((rotation) => { | ||||
|             const cone = element.getElementsByClassName("direction-svg")[0] as HTMLElement | ||||
|             cone.style.transform = `rotate(${rotation}deg)` | ||||
|         }) | ||||
| 
 | ||||
|         this.RegisterTriggers(element) | ||||
|         element.style.overflow = "hidden" | ||||
|         element.style.display = "block" | ||||
| 
 | ||||
|         return element | ||||
|     } | ||||
| 
 | ||||
|     private RegisterTriggers(htmlElement: HTMLElement) { | ||||
|         const self = this | ||||
| 
 | ||||
|         function onPosChange(x: number, y: number) { | ||||
|             const rect = htmlElement.getBoundingClientRect() | ||||
|             const dx = -(rect.left + rect.right) / 2 + x | ||||
|             const dy = (rect.top + rect.bottom) / 2 - y | ||||
|             const angle = (180 * Math.atan2(dy, dx)) / Math.PI | ||||
|             const angleGeo = Math.floor((450 - angle) % 360) | ||||
|             self.value.setData("" + angleGeo) | ||||
|         } | ||||
| 
 | ||||
|         htmlElement.ontouchmove = (ev: TouchEvent) => { | ||||
|             onPosChange(ev.touches[0].clientX, ev.touches[0].clientY) | ||||
|             ev.preventDefault() | ||||
|         } | ||||
| 
 | ||||
|         htmlElement.ontouchstart = (ev: TouchEvent) => { | ||||
|             onPosChange(ev.touches[0].clientX, ev.touches[0].clientY) | ||||
|         } | ||||
| 
 | ||||
|         let isDown = false | ||||
| 
 | ||||
|         htmlElement.onmousedown = (ev: MouseEvent) => { | ||||
|             isDown = true | ||||
|             onPosChange(ev.clientX, ev.clientY) | ||||
|             ev.preventDefault() | ||||
|         } | ||||
| 
 | ||||
|         htmlElement.onmouseup = (ev) => { | ||||
|             isDown = false | ||||
|             ev.preventDefault() | ||||
|         } | ||||
| 
 | ||||
|         htmlElement.onmousemove = (ev: MouseEvent) => { | ||||
|             if (isDown) { | ||||
|                 onPosChange(ev.clientX, ev.clientY) | ||||
|             } | ||||
|             ev.preventDefault() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| import { InputElement } from "./InputElement" | ||||
| import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Combine from "../Base/Combine" | ||||
| import Slider from "./Slider" | ||||
| import { ClickableToggle } from "./Toggle" | ||||
|  |  | |||
|  | @ -1,24 +1,17 @@ | |||
| import { ReadonlyInputElement } from "./InputElement" | ||||
| import Loc from "../../Models/Loc" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Minimap, { MinimapObj } from "../Base/Minimap" | ||||
| import BaseLayer from "../../Models/BaseLayer" | ||||
| import Combine from "../Base/Combine" | ||||
| import Svg from "../../Svg" | ||||
| import { GeoOperations } from "../../Logic/GeoOperations" | ||||
| import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" | ||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { BBox } from "../../Logic/BBox" | ||||
| import { FixedUiElement } from "../Base/FixedUiElement" | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" | ||||
| import BaseUIElement from "../BaseUIElement" | ||||
| import Toggle from "./Toggle" | ||||
| import matchpoint from "../../assets/layers/matchpoint/matchpoint.json" | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import { ElementStorage } from "../../Logic/ElementStorage" | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers" | ||||
| import { RelationId, WayId } from "../../Models/OsmFeature" | ||||
| import { Feature, LineString, Polygon } from "geojson" | ||||
| import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject" | ||||
|  | @ -313,10 +306,6 @@ export default class LocationInput | |||
|                 [this.map.leafletMap] | ||||
|             ) | ||||
| 
 | ||||
|             const animatedHand = Svg.hand_ui() | ||||
|                 .SetStyle("width: 2rem; height: unset;") | ||||
|                 .SetClass("hand-drag-animation block pointer-events-none") | ||||
| 
 | ||||
|             return new Combine([ | ||||
|                 new Combine([ | ||||
|                     Svg.move_arrows_ui() | ||||
|  | @ -328,10 +317,6 @@ export default class LocationInput | |||
|                         "background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%; opacity: 0.5" | ||||
|                     ), | ||||
| 
 | ||||
|                 new Toggle(undefined, animatedHand, hasMoved) | ||||
|                     .SetClass("block w-0 h-0 z-10 relative") | ||||
|                     .SetStyle("left: calc(50% + 3rem); top: calc(50% + 2rem); opacity: 0.7"), | ||||
| 
 | ||||
|                 this.map.SetClass("z-0 relative block w-full h-full bg-gray-100"), | ||||
|             ]).ConstructElement() | ||||
|         } catch (e) { | ||||
|  | @ -341,11 +326,4 @@ export default class LocationInput | |||
|                 .ConstructElement() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     TakeScreenshot(format: "image"): Promise<string> | ||||
|     TakeScreenshot(format: "blob"): Promise<Blob> | ||||
|     TakeScreenshot(format: "image" | "blob"): Promise<string | Blob> | ||||
|     TakeScreenshot(format: "image" | "blob"): Promise<string | Blob> { | ||||
|         return this.map.TakeScreenshot(format) | ||||
|     } | ||||
| } | ||||
|  |  | |||
							
								
								
									
										1
									
								
								UI/Input/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								UI/Input/README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| This is the old, deprecated directory. New, SVelte-based items go into `InputElement` | ||||
|  | @ -2,7 +2,6 @@ import { InputElement } from "./InputElement" | |||
| import { UIEventSource } from "../../Logic/UIEventSource" | ||||
| 
 | ||||
| export default class SimpleDatePicker extends InputElement<string> { | ||||
|     IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|     private readonly value: UIEventSource<string> | ||||
|     private readonly _element: HTMLElement | ||||
| 
 | ||||
|  |  | |||
|  | @ -50,10 +50,6 @@ export class TextField extends InputElement<string> { | |||
|         return this.value | ||||
|     } | ||||
| 
 | ||||
|     GetRawValue(): UIEventSource<string> { | ||||
|         return this._rawValue | ||||
|     } | ||||
| 
 | ||||
|     IsValid(t: string): boolean { | ||||
|         if (t === undefined || t === null) { | ||||
|             return false | ||||
|  |  | |||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										70
									
								
								UI/InputElement/Helpers/DirectionInput.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								UI/InputElement/Helpers/DirectionInput.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | |||
| <script lang="ts"> | ||||
|   import { UIEventSource } from "../../../Logic/UIEventSource"; | ||||
|   import type { MapProperties } from "../../../Models/MapProperties"; | ||||
|   import { Map as MlMap } from "maplibre-gl"; | ||||
|   import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor"; | ||||
|   import MaplibreMap from "../../Map/MaplibreMap.svelte"; | ||||
|   import ToSvelte from "../../Base/ToSvelte.svelte"; | ||||
|   import Svg from "../../../Svg.js"; | ||||
| 
 | ||||
|   /** | ||||
|    * A visualisation to pick a direction on a map background | ||||
|    */ | ||||
|   export let value: UIEventSource<undefined | number>; | ||||
|   export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> }; | ||||
|   let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined); | ||||
|   let mla = new MapLibreAdaptor(map, mapProperties); | ||||
|   mla.allowMoving.setData(false) | ||||
|   mla.allowZooming.setData(false) | ||||
|   let directionElem: HTMLElement | undefined; | ||||
|   $: value.addCallbackAndRunD(degrees => { | ||||
|     console.log("Degrees are", degrees, directionElem); | ||||
|     if (directionElem === undefined) { | ||||
|       return; | ||||
|     } | ||||
|     directionElem.style.rotate = degrees + "deg"; | ||||
|   }); | ||||
| 
 | ||||
|   let mainElem : HTMLElement | ||||
|   function onPosChange(x: number, y: number) { | ||||
|     const rect = mainElem.getBoundingClientRect(); | ||||
|     const dx = -(rect.left + rect.right) / 2 + x; | ||||
|     const dy = (rect.top + rect.bottom) / 2 - y; | ||||
|     const angle = (180 * Math.atan2(dy, dx)) / Math.PI; | ||||
|     const angleGeo = Math.floor((450 - angle) % 360); | ||||
|     value.setData(angleGeo); | ||||
|   } | ||||
| 
 | ||||
|   let isDown = false; | ||||
| </script> | ||||
| 
 | ||||
| <div bind:this={mainElem} class="relative w-48 h-48 cursor-pointer overflow-hidden" | ||||
|      on:click={e => onPosChange(e.x, e.y)} | ||||
|      on:mousedown={e => { | ||||
|          isDown = true | ||||
|          onPosChange(e.clientX, e.clientY) | ||||
|        } } | ||||
|      on:mousemove={e => { | ||||
|       if(isDown){ | ||||
|       onPosChange(e.clientX, e.clientY) | ||||
|          | ||||
|       }}} | ||||
| 
 | ||||
|      on:mouseup={() => { | ||||
|          isDown = false | ||||
|        } } | ||||
|      on:touchmove={e => onPosChange(e.touches[0].clientX, e.touches[0].clientY)} | ||||
| 
 | ||||
| 
 | ||||
|      on:touchstart={e => onPosChange(e.touches[0].clientX, e.touches[0].clientY)}> | ||||
|   <div class="w-full h-full absolute top-0 left-0 cursor-pointer"> | ||||
|     <MaplibreMap {map} attribution={false}></MaplibreMap> | ||||
|   </div> | ||||
| 
 | ||||
|   <div bind:this={directionElem} class="absolute w-full h-full top-0 left-0 border border-red-500"> | ||||
| 
 | ||||
|     <ToSvelte construct={ Svg.direction_stroke_svg}> | ||||
| 
 | ||||
|     </ToSvelte> | ||||
|   </div> | ||||
| </div> | ||||
							
								
								
									
										42
									
								
								UI/InputElement/Helpers/LocationInput.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								UI/InputElement/Helpers/LocationInput.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | |||
| <script lang="ts"> | ||||
|   import { Store, UIEventSource } from "../../../Logic/UIEventSource"; | ||||
|   import type { MapProperties } from "../../../Models/MapProperties"; | ||||
|   import { Map as MlMap } from "maplibre-gl"; | ||||
|   import { MapLibreAdaptor } from "../../Map/MapLibreAdaptor"; | ||||
|   import MaplibreMap from "../../Map/MaplibreMap.svelte"; | ||||
|   import Svg from "../../../Svg"; | ||||
|   import ToSvelte from "../../Base/ToSvelte.svelte"; | ||||
|   import DragInvitation from "../../Base/DragInvitation.svelte"; | ||||
| 
 | ||||
|   /** | ||||
|    * A visualisation to pick a direction on a map background | ||||
|    */ | ||||
|   export let value: UIEventSource<{lon: number, lat: number}>; | ||||
|   export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> }; | ||||
|   /** | ||||
|    * Called when setup is done, cna be used to add layrs to the map | ||||
|    */ | ||||
|   export let onCreated : (value: Store<{lon: number, lat: number}> , map: Store<MlMap>, mapProperties: MapProperties ) => void | ||||
|    | ||||
|   let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined); | ||||
|   let mla = new MapLibreAdaptor(map, mapProperties); | ||||
|   mla.allowMoving.setData(true) | ||||
|   mla.allowZooming.setData(true) | ||||
| 
 | ||||
|   if(onCreated){ | ||||
|     onCreated(value, map, mla) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <div class="relative h-32 cursor-pointer overflow-hidden"> | ||||
|   <div class="w-full h-full absolute top-0 left-0 cursor-pointer"> | ||||
|     <MaplibreMap {map} attribution={false}></MaplibreMap> | ||||
|   </div> | ||||
| 
 | ||||
|   <div class="w-full h-full absolute top-0 left-0 p-8 pointer-events-none opacity-50"> | ||||
|       <ToSvelte construct={() => Svg.move_arrows_svg().SetClass("h-full")}></ToSvelte> | ||||
|   </div> | ||||
|    | ||||
|   <DragInvitation></DragInvitation> | ||||
| 
 | ||||
| </div> | ||||
							
								
								
									
										13
									
								
								UI/InputElement/InputHelper.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								UI/InputElement/InputHelper.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,13 @@ | |||
| <script lang="ts"> | ||||
|   /** | ||||
|    * Constructs an input helper element for the given type. | ||||
|    * Note that all values are stringified | ||||
|    */ | ||||
|    | ||||
|   import { AvailableInputHelperType } from "./InputHelpers"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
| 
 | ||||
|   export let type : AvailableInputHelperType | ||||
|   export let value : UIEventSource<string> | ||||
|    | ||||
| </script> | ||||
							
								
								
									
										16
									
								
								UI/InputElement/InputHelpers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								UI/InputElement/InputHelpers.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| import { AvailableRasterLayers } from "../../Models/RasterLayers" | ||||
| 
 | ||||
| export type AvailableInputHelperType = typeof InputHelpers.AvailableInputHelpers[number] | ||||
| 
 | ||||
| export default class InputHelpers { | ||||
|     public static readonly AvailableInputHelpers = [] as const | ||||
|     /** | ||||
|      * To port | ||||
|      * direction | ||||
|      * opening_hours | ||||
|      * color | ||||
|      * length | ||||
|      * date | ||||
|      * wikidata | ||||
|      */ | ||||
| } | ||||
							
								
								
									
										119
									
								
								UI/InputElement/ValidatedTextField.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								UI/InputElement/ValidatedTextField.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,119 @@ | |||
| import BaseUIElement from "../BaseUIElement" | ||||
| import Combine from "../Base/Combine" | ||||
| import Title from "../Base/Title" | ||||
| import Translations from "../i18n/Translations" | ||||
| import { Translation } from "../i18n/Translation" | ||||
| import WikidataValidator from "./Validators/WikidataValidator" | ||||
| import StringValidator from "./Validators/StringValidator" | ||||
| import TextValidator from "./Validators/TextValidator" | ||||
| import DateValidator from "./Validators/DateValidator" | ||||
| import LengthValidator from "./Validators/LengthValidator" | ||||
| import IntValidator from "./Validators/IntValidator" | ||||
| import EmailValidator from "./Validators/EmailValidator" | ||||
| import DirectionValidator from "./Validators/DirectionValidator" | ||||
| import NatValidator from "./Validators/NatValidator" | ||||
| import OpeningHoursValidator from "./Validators/OpeningHoursValidator" | ||||
| import PFloatValidator from "./Validators/PFloatValidator" | ||||
| import ColorValidator from "./Validators/ColorValidator" | ||||
| import PhoneValidator from "./Validators/PhoneValidator" | ||||
| import UrlValidator from "./Validators/UrlValidator" | ||||
| import FloatValidator from "./Validators/FloatValidator" | ||||
| import PNatValidator from "./Validators/PNatValidator" | ||||
| 
 | ||||
| /** | ||||
|  * A 'TextFieldValidator' contains various methods to check and cleanup an entered value or to give feedback. | ||||
|  * They also double as an index of supported types for textfields in MapComplete | ||||
|  */ | ||||
| export abstract class Validator { | ||||
|     public readonly name: string | ||||
|     /* | ||||
|      * An explanation for the theme builder. | ||||
|      * This can indicate which special input element is used, ... | ||||
|      * */ | ||||
|     public readonly explanation: string | ||||
|     /** | ||||
|      * What HTML-inputmode to use | ||||
|      */ | ||||
|     public readonly inputmode?: string | ||||
| 
 | ||||
|     constructor(name: string, explanation: string | BaseUIElement, inputmode?: string) { | ||||
|         this.name = name | ||||
|         this.inputmode = inputmode | ||||
|         if (this.name.endsWith("textfield")) { | ||||
|             this.name = this.name.substr(0, this.name.length - "TextField".length) | ||||
|         } | ||||
|         if (this.name.endsWith("textfielddef")) { | ||||
|             this.name = this.name.substr(0, this.name.length - "TextFieldDef".length) | ||||
|         } | ||||
|         if (typeof explanation === "string") { | ||||
|             this.explanation = explanation | ||||
|         } else { | ||||
|             this.explanation = explanation.AsMarkdown() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets a piece of feedback. By default, validation.<type> will be used, resulting in a generic 'not a valid <type>'. | ||||
|      * However, inheritors might overwrite this to give more specific feedback | ||||
|      * @param s | ||||
|      */ | ||||
|     public getFeedback(s: string): Translation { | ||||
|         const tr = Translations.t.validation[this.name] | ||||
|         if (tr !== undefined) { | ||||
|             return tr["feedback"] | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public isValid(string: string, requestCountry: () => string): boolean { | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     public reformat(s: string, country?: () => string): string { | ||||
|         return s | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default class Validators { | ||||
|     private static readonly AllValidators: ReadonlyArray<Validator> = [ | ||||
|         new StringValidator(), | ||||
|         new TextValidator(), | ||||
|         new DateValidator(), | ||||
|         new NatValidator(), | ||||
|         new IntValidator(), | ||||
|         new LengthValidator(), | ||||
|         new DirectionValidator(), | ||||
|         new WikidataValidator(), | ||||
|         new PNatValidator(), | ||||
|         new FloatValidator(), | ||||
|         new PFloatValidator(), | ||||
|         new EmailValidator(), | ||||
|         new UrlValidator(), | ||||
|         new PhoneValidator(), | ||||
|         new OpeningHoursValidator(), | ||||
|         new ColorValidator(), | ||||
|     ] | ||||
|     public static allTypes: Map<string, Validator> = Validators.allTypesDict() | ||||
| 
 | ||||
|     public static HelpText(): BaseUIElement { | ||||
|         const explanations: BaseUIElement[] = Validators.AllValidators.map((type) => | ||||
|             new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col") | ||||
|         ) | ||||
|         return new Combine([ | ||||
|             new Title("Available types for text fields", 1), | ||||
|             "The listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them", | ||||
|             ...explanations, | ||||
|         ]).SetClass("flex flex-col") | ||||
|     } | ||||
| 
 | ||||
|     public static AvailableTypes(): string[] { | ||||
|         return Validators.AllValidators.map((tp) => tp.name) | ||||
|     } | ||||
| 
 | ||||
|     private static allTypesDict(): Map<string, Validator> { | ||||
|         const types = new Map<string, Validator>() | ||||
|         for (const tp of Validators.AllValidators) { | ||||
|             types.set(tp.name, tp) | ||||
|         } | ||||
|         return types | ||||
|     } | ||||
| } | ||||
							
								
								
									
										7
									
								
								UI/InputElement/Validators/ColorValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								UI/InputElement/Validators/ColorValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| import { Validator } from "../ValidatedTextField" | ||||
| 
 | ||||
| export default class ColorValidator extends Validator { | ||||
|     constructor() { | ||||
|         super("color", "Shows a color picker") | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								UI/InputElement/Validators/DateValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								UI/InputElement/Validators/DateValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| import { Validator } from "../ValidatedTextField" | ||||
| 
 | ||||
| export default class DateValidator extends Validator { | ||||
|     constructor() { | ||||
|         super("date", "A date with date picker") | ||||
|     } | ||||
| 
 | ||||
|     isValid(str: string): boolean { | ||||
|         return !isNaN(new Date(str).getTime()) | ||||
|     } | ||||
| 
 | ||||
|     reformat(str: string) { | ||||
|         const d = new Date(str) | ||||
|         let month = "" + (d.getMonth() + 1) | ||||
|         let day = "" + d.getDate() | ||||
|         const year = d.getFullYear() | ||||
| 
 | ||||
|         if (month.length < 2) month = "0" + month | ||||
|         if (day.length < 2) day = "0" + day | ||||
| 
 | ||||
|         return [year, month, day].join("-") | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								UI/InputElement/Validators/DirectionValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								UI/InputElement/Validators/DirectionValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| import { Validator } from "../ValidatedTextField" | ||||
| import IntValidator from "./IntValidator"; | ||||
| 
 | ||||
| export default class DirectionValidator extends IntValidator { | ||||
|     constructor() { | ||||
|         super( | ||||
|             "direction", | ||||
|             "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)" | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     reformat(str): string { | ||||
|         const n = Number(str) % 360 | ||||
|         return "" + n | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										39
									
								
								UI/InputElement/Validators/EmailValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								UI/InputElement/Validators/EmailValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| import { Validator } from "../ValidatedTextField.js" | ||||
| import { Translation } from "../../i18n/Translation.js" | ||||
| import Translations from "../../i18n/Translations.js" | ||||
| import * as emailValidatorLibrary from "email-validator" | ||||
| export default class EmailValidator extends Validator { | ||||
|     constructor() { | ||||
|         super("email", "An email adress", "email") | ||||
|     } | ||||
| 
 | ||||
|     isValid = (str) => { | ||||
|         if (str === undefined) { | ||||
|             return false | ||||
|         } | ||||
|         str = str.trim() | ||||
|         if (str.startsWith("mailto:")) { | ||||
|             str = str.substring("mailto:".length) | ||||
|         } | ||||
|         return emailValidatorLibrary.validate(str) | ||||
|     } | ||||
| 
 | ||||
|     reformat = (str) => { | ||||
|         if (str === undefined) { | ||||
|             return undefined | ||||
|         } | ||||
|         str = str.trim() | ||||
|         if (str.startsWith("mailto:")) { | ||||
|             str = str.substring("mailto:".length) | ||||
|         } | ||||
|         return str | ||||
|     } | ||||
| 
 | ||||
|     getFeedback(s: string): Translation { | ||||
|         if (s.indexOf("@") < 0) { | ||||
|             return Translations.t.validation.email.noAt | ||||
|         } | ||||
| 
 | ||||
|         return super.getFeedback(s) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										27
									
								
								UI/InputElement/Validators/FloatValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								UI/InputElement/Validators/FloatValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| import { Translation } from "../../i18n/Translation" | ||||
| import Translations from "../../i18n/Translations" | ||||
| import { Validator } from "../ValidatedTextField" | ||||
| 
 | ||||
| export default class FloatValidator extends Validator { | ||||
|     inputmode = "decimal" | ||||
| 
 | ||||
|     constructor(name?: string, explanation?: string) { | ||||
|         super(name ?? "float", explanation ?? "A decimal number", "decimal") | ||||
|     } | ||||
| 
 | ||||
|     isValid(str) { | ||||
|         return !isNaN(Number(str)) && !str.endsWith(".") && !str.endsWith(",") | ||||
|     } | ||||
| 
 | ||||
|     reformat(str): string { | ||||
|         return "" + Number(str) | ||||
|     } | ||||
| 
 | ||||
|     getFeedback(s: string): Translation { | ||||
|         if (isNaN(Number(s))) { | ||||
|             return Translations.t.validation.nat.notANumber | ||||
|         } | ||||
| 
 | ||||
|         return undefined | ||||
|     } | ||||
| } | ||||
							
								
								
									
										29
									
								
								UI/InputElement/Validators/IntValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								UI/InputElement/Validators/IntValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| import { Translation } from "../../i18n/Translation" | ||||
| import Translations from "../../i18n/Translations" | ||||
| import { Validator } from "../ValidatedTextField" | ||||
| 
 | ||||
| export default class IntValidator extends Validator { | ||||
|     constructor(name?: string, explanation?: string) { | ||||
|         super( | ||||
|             name ?? "int", | ||||
|             explanation ?? "A whole number, either positive, negative or zero", | ||||
|             "numeric" | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     isValid(str): boolean { | ||||
|         str = "" + str | ||||
|         return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) | ||||
|     } | ||||
| 
 | ||||
|     getFeedback(s: string): Translation { | ||||
|         const n = Number(s) | ||||
|         if (isNaN(n)) { | ||||
|             return Translations.t.validation.nat.notANumber | ||||
|         } | ||||
|         if (Math.floor(n) !== n) { | ||||
|             return Translations.t.validation.nat.mustBeWhole | ||||
|         } | ||||
|         return undefined | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								UI/InputElement/Validators/LengthValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								UI/InputElement/Validators/LengthValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| import { Validator } from "../ValidatedTextField" | ||||
| 
 | ||||
| export default class LengthValidator extends Validator { | ||||
|     constructor() { | ||||
|         super( | ||||
|             "distance", | ||||
|             'A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]', | ||||
|             "decimal" | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     isValid = (str) => { | ||||
|         const t = Number(str) | ||||
|         return !isNaN(t) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										30
									
								
								UI/InputElement/Validators/NatValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								UI/InputElement/Validators/NatValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| import IntValidator from "./IntValidator" | ||||
| import { Translation } from "../../i18n/Translation" | ||||
| import Translations from "../../i18n/Translations" | ||||
| 
 | ||||
| export default class NatValidator extends IntValidator { | ||||
|     constructor(name?: string, explanation?: string) { | ||||
|         super(name ?? "nat", explanation ?? "A  whole, positive number or zero") | ||||
|     } | ||||
| 
 | ||||
|     isValid(str): boolean { | ||||
|         if (str === undefined) { | ||||
|             return false | ||||
|         } | ||||
|         str = "" + str | ||||
| 
 | ||||
|         return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 | ||||
|     } | ||||
| 
 | ||||
|     getFeedback(s: string): Translation { | ||||
|         const spr = super.getFeedback(s) | ||||
|         if (spr !== undefined) { | ||||
|             return spr | ||||
|         } | ||||
|         const n = Number(s) | ||||
|         if (n < 0) { | ||||
|             return Translations.t.validation.nat.mustBePositive | ||||
|         } | ||||
|         return undefined | ||||
|     } | ||||
| } | ||||
							
								
								
									
										54
									
								
								UI/InputElement/Validators/OpeningHoursValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								UI/InputElement/Validators/OpeningHoursValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| import { Validator } from "../ValidatedTextField" | ||||
| import Combine from "../../Base/Combine" | ||||
| import Title from "../../Base/Title" | ||||
| import Table from "../../Base/Table" | ||||
| 
 | ||||
| export default class OpeningHoursValidator extends Validator { | ||||
|     constructor() { | ||||
|         super( | ||||
|             "opening_hours", | ||||
|             new Combine([ | ||||
|                 "Has extra elements to easily input when a POI is opened.", | ||||
|                 new Title("Helper arguments"), | ||||
|                 new Table( | ||||
|                     ["name", "doc"], | ||||
|                     [ | ||||
|                         [ | ||||
|                             "options", | ||||
|                             new Combine([ | ||||
|                                 "A JSON-object of type `{ prefix: string, postfix: string }`. ", | ||||
|                                 new Table( | ||||
|                                     ["subarg", "doc"], | ||||
|                                     [ | ||||
|                                         [ | ||||
|                                             "prefix", | ||||
|                                             "Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse.", | ||||
|                                         ], | ||||
|                                         [ | ||||
|                                             "postfix", | ||||
|                                             "Piece of text that will always be added to the end of the generated opening hours", | ||||
|                                         ], | ||||
|                                     ] | ||||
|                                 ), | ||||
|                             ]), | ||||
|                         ], | ||||
|                     ] | ||||
|                 ), | ||||
|                 new Title("Example usage"), | ||||
|                 "To add a conditional (based on time) access restriction:\n\n```\n" + | ||||
|                     ` | ||||
| "freeform": { | ||||
|     "key": "access:conditional", | ||||
|     "type": "opening_hours", | ||||
|     "helperArgs": [ | ||||
|         { | ||||
|           "prefix":"no @ (", | ||||
|           "postfix":")" | ||||
|         } | ||||
|     ] | ||||
| }` +
 | ||||
|                     "\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`", | ||||
|             ]) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								UI/InputElement/Validators/PFloatValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								UI/InputElement/Validators/PFloatValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| import { Translation } from "../../i18n/Translation" | ||||
| import Translations from "../../i18n/Translations" | ||||
| import { Validator } from "../ValidatedTextField" | ||||
| 
 | ||||
| export default class PFloatValidator extends Validator { | ||||
|     constructor() { | ||||
|         super("pfloat", "A positive decimal number or zero") | ||||
|     } | ||||
| 
 | ||||
|     isValid = (str) => | ||||
|         !isNaN(Number(str)) && Number(str) >= 0 && !str.endsWith(".") && !str.endsWith(",") | ||||
| 
 | ||||
|     getFeedback(s: string): Translation { | ||||
|         const spr = super.getFeedback(s) | ||||
|         if (spr !== undefined) { | ||||
|             return spr | ||||
|         } | ||||
|         if (Number(s) < 0) { | ||||
|             return Translations.t.validation.nat.mustBePositive | ||||
|         } | ||||
|         return undefined | ||||
|     } | ||||
| } | ||||
							
								
								
									
										27
									
								
								UI/InputElement/Validators/PNatValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								UI/InputElement/Validators/PNatValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,27 @@ | |||
| import { Translation } from "../../i18n/Translation" | ||||
| import Translations from "../../i18n/Translations" | ||||
| import NatValidator from "./NatValidator" | ||||
| 
 | ||||
| export default class PNatValidator extends NatValidator { | ||||
|     constructor() { | ||||
|         super("pnat", "A strict positive number") | ||||
|     } | ||||
| 
 | ||||
|     getFeedback(s: string): Translation { | ||||
|         const spr = super.getFeedback(s) | ||||
|         if (spr !== undefined) { | ||||
|             return spr | ||||
|         } | ||||
|         if (Number(s) === 0) { | ||||
|             return Translations.t.validation.pnat.noZero | ||||
|         } | ||||
|         return undefined | ||||
|     } | ||||
| 
 | ||||
|     isValid = (str) => { | ||||
|         if (!super.isValid(str)) { | ||||
|             return false | ||||
|         } | ||||
|         return Number(str) > 0 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										32
									
								
								UI/InputElement/Validators/PhoneValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								UI/InputElement/Validators/PhoneValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| import { Validator } from "../ValidatedTextField" | ||||
| import { parsePhoneNumberFromString } from "libphonenumber-js" | ||||
| 
 | ||||
| export default class PhoneValidator extends Validator { | ||||
|     constructor() { | ||||
|         super("phone", "A phone number", "tel") | ||||
|     } | ||||
| 
 | ||||
|     isValid(str, country: () => string): boolean { | ||||
|         if (str === undefined) { | ||||
|             return false | ||||
|         } | ||||
|         if (str.startsWith("tel:")) { | ||||
|             str = str.substring("tel:".length) | ||||
|         } | ||||
|         let countryCode = undefined | ||||
|         if (country !== undefined) { | ||||
|             countryCode = country()?.toUpperCase() | ||||
|         } | ||||
|         return parsePhoneNumberFromString(str, countryCode)?.isValid() ?? false | ||||
|     } | ||||
| 
 | ||||
|     reformat = (str, country: () => string) => { | ||||
|         if (str.startsWith("tel:")) { | ||||
|             str = str.substring("tel:".length) | ||||
|         } | ||||
|         return parsePhoneNumberFromString( | ||||
|             str, | ||||
|             country()?.toUpperCase() as any | ||||
|         )?.formatInternational() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										8
									
								
								UI/InputElement/Validators/StringValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								UI/InputElement/Validators/StringValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| import { Validator } from "../ValidatedTextField" | ||||
| 
 | ||||
| export default class StringValidator extends Validator { | ||||
|     constructor() { | ||||
|         super("string", "A simple piece of text") | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										7
									
								
								UI/InputElement/Validators/TextValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								UI/InputElement/Validators/TextValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| import { Validator } from "../ValidatedTextField" | ||||
| 
 | ||||
| export default class TextValidator extends Validator { | ||||
|     constructor() { | ||||
|         super("text", "A longer piece of text. Uses an textArea instead of a textField", "text") | ||||
|     } | ||||
| } | ||||
							
								
								
									
										75
									
								
								UI/InputElement/Validators/UrlValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								UI/InputElement/Validators/UrlValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,75 @@ | |||
| import { Validator } from "../ValidatedTextField" | ||||
| 
 | ||||
| export default class UrlValidator extends Validator { | ||||
|     constructor() { | ||||
|         super( | ||||
|             "url", | ||||
|             "The validatedTextField will format URLs to always be valid and have a https://-header (even though the 'https'-part will be hidden from the user. Furthermore, some tracking parameters will be removed", | ||||
|             "url" | ||||
|         ) | ||||
|     } | ||||
|     reformat(str: string): string { | ||||
|         try { | ||||
|             let url: URL | ||||
|             // str = str.toLowerCase() // URLS are case sensitive. Lowercasing them might break some URLS. See #763
 | ||||
|             if ( | ||||
|                 !str.startsWith("http://") && | ||||
|                 !str.startsWith("https://") && | ||||
|                 !str.startsWith("http:") | ||||
|             ) { | ||||
|                 url = new URL("https://" + str) | ||||
|             } else { | ||||
|                 url = new URL(str) | ||||
|             } | ||||
|             const blacklistedTrackingParams = [ | ||||
|                 "fbclid", // Oh god, how I hate the fbclid. Let it burn, burn in hell!
 | ||||
|                 "gclid", | ||||
|                 "cmpid", | ||||
|                 "agid", | ||||
|                 "utm", | ||||
|                 "utm_source", | ||||
|                 "utm_medium", | ||||
|                 "campaignid", | ||||
|                 "campaign", | ||||
|                 "AdGroupId", | ||||
|                 "AdGroup", | ||||
|                 "TargetId", | ||||
|                 "msclkid", | ||||
|             ] | ||||
|             for (const dontLike of blacklistedTrackingParams) { | ||||
|                 url.searchParams.delete(dontLike.toLowerCase()) | ||||
|             } | ||||
|             let cleaned = url.toString() | ||||
|             if (cleaned.endsWith("/") && !str.endsWith("/")) { | ||||
|                 // Do not add a trailing '/' if it wasn't typed originally
 | ||||
|                 cleaned = cleaned.substr(0, cleaned.length - 1) | ||||
|             } | ||||
| 
 | ||||
|             if (!str.startsWith("http") && cleaned.startsWith("https://")) { | ||||
|                 cleaned = cleaned.substr("https://".length) | ||||
|             } | ||||
| 
 | ||||
|             return cleaned | ||||
|         } catch (e) { | ||||
|             console.error(e) | ||||
|             return undefined | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     isValid(str: string): boolean { | ||||
|         try { | ||||
|             if ( | ||||
|                 !str.startsWith("http://") && | ||||
|                 !str.startsWith("https://") && | ||||
|                 !str.startsWith("http:") | ||||
|             ) { | ||||
|                 str = "https://" + str | ||||
|             } | ||||
|             const url = new URL(str) | ||||
|             const dotIndex = url.host.indexOf(".") | ||||
|             return dotIndex > 0 && url.host[url.host.length - 1] !== "." | ||||
|         } catch (e) { | ||||
|             return false | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										179
									
								
								UI/InputElement/Validators/WikidataValidator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								UI/InputElement/Validators/WikidataValidator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,179 @@ | |||
| import Combine from "../../Base/Combine" | ||||
| import Title from "../../Base/Title" | ||||
| import Table from "../../Base/Table" | ||||
| import Wikidata from "../../../Logic/Web/Wikidata" | ||||
| import { UIEventSource } from "../../../Logic/UIEventSource" | ||||
| import Locale from "../../i18n/Locale" | ||||
| import { Utils } from "../../../Utils" | ||||
| import WikidataSearchBox from "../../Wikipedia/WikidataSearchBox" | ||||
| import { Validator } from "../ValidatedTextField" | ||||
| 
 | ||||
| export default class WikidataValidator extends Validator { | ||||
|     constructor() { | ||||
|         super( | ||||
|             "wikidata", | ||||
|             new Combine([ | ||||
|                 "A wikidata identifier, e.g. Q42.", | ||||
|                 new Title("Helper arguments"), | ||||
|                 new Table( | ||||
|                     ["name", "doc"], | ||||
|                     [ | ||||
|                         ["key", "the value of this tag will initialize search (default: name)"], | ||||
|                         [ | ||||
|                             "options", | ||||
|                             new Combine([ | ||||
|                                 "A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.", | ||||
|                                 new Table( | ||||
|                                     ["subarg", "doc"], | ||||
|                                     [ | ||||
|                                         [ | ||||
|                                             "removePrefixes", | ||||
|                                             "remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes", | ||||
|                                         ], | ||||
|                                         [ | ||||
|                                             "removePostfixes", | ||||
|                                             "remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.", | ||||
|                                         ], | ||||
|                                         [ | ||||
|                                             "instanceOf", | ||||
|                                             "A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans", | ||||
|                                         ], | ||||
|                                         [ | ||||
|                                             "notInstanceof", | ||||
|                                             "A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results", | ||||
|                                         ], | ||||
|                                     ] | ||||
|                                 ), | ||||
|                             ]), | ||||
|                         ], | ||||
|                     ] | ||||
|                 ), | ||||
|                 new Title("Example usage"), | ||||
|                 `The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
 | ||||
| 
 | ||||
| \`\`\`json
 | ||||
| "freeform": { | ||||
|     "key": "name:etymology:wikidata", | ||||
|     "type": "wikidata", | ||||
|     "helperArgs": [ | ||||
|         "name", | ||||
|         { | ||||
|             "removePostfixes": {"en": [ | ||||
|                 "street", | ||||
|                 "boulevard", | ||||
|                 "path", | ||||
|                 "square", | ||||
|                 "plaza", | ||||
|             ], | ||||
|              "nl": ["straat","plein","pad","weg",laan"], | ||||
|              "fr":["route (de|de la|de l'| de le)"] | ||||
|              }, | ||||
| 
 | ||||
|             "#": "Remove streets and parks from the search results:" | ||||
|              "notInstanceOf": ["Q79007","Q22698"] | ||||
|         } | ||||
| 
 | ||||
|     ] | ||||
| } | ||||
| \`\`\` | ||||
| 
 | ||||
| Another example is to search for species and trees: | ||||
| 
 | ||||
| \`\`\`json
 | ||||
|  "freeform": { | ||||
|         "key": "species:wikidata", | ||||
|         "type": "wikidata", | ||||
|         "helperArgs": [ | ||||
|           "species", | ||||
|           { | ||||
|           "instanceOf": [10884, 16521] | ||||
|         }] | ||||
|       } | ||||
| \`\`\` | ||||
| `,
 | ||||
|             ]) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public isValid(str): boolean { | ||||
|         if (str === undefined) { | ||||
|             return false | ||||
|         } | ||||
|         if (str.length <= 2) { | ||||
|             return false | ||||
|         } | ||||
|         return !str.split(";").some((str) => Wikidata.ExtractKey(str) === undefined) | ||||
|     } | ||||
| 
 | ||||
|     public reformat(str) { | ||||
|         if (str === undefined) { | ||||
|             return undefined | ||||
|         } | ||||
|         let out = str | ||||
|             .split(";") | ||||
|             .map((str) => Wikidata.ExtractKey(str)) | ||||
|             .join("; ") | ||||
|         if (str.endsWith(";")) { | ||||
|             out = out + ";" | ||||
|         } | ||||
|         return out | ||||
|     } | ||||
| 
 | ||||
|     public inputHelper(currentValue, inputHelperOptions) { | ||||
|         const args = inputHelperOptions.args ?? [] | ||||
|         const searchKey = args[0] ?? "name" | ||||
| 
 | ||||
|         const searchFor = <string>( | ||||
|             (inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "") | ||||
|         ) | ||||
| 
 | ||||
|         let searchForValue: UIEventSource<string> = new UIEventSource(searchFor) | ||||
|         const options: any = args[1] | ||||
|         if (searchFor !== undefined && options !== undefined) { | ||||
|             const prefixes = <string[] | Record<string, string[]>>options["removePrefixes"] ?? [] | ||||
|             const postfixes = <string[] | Record<string, string[]>>options["removePostfixes"] ?? [] | ||||
|             const defaultValueCandidate = Locale.language.map((lg) => { | ||||
|                 const prefixesUnrwapped: RegExp[] = ( | ||||
|                     Array.isArray(prefixes) ? prefixes : prefixes[lg] ?? [] | ||||
|                 ).map((s) => new RegExp("^" + s, "i")) | ||||
|                 const postfixesUnwrapped: RegExp[] = ( | ||||
|                     Array.isArray(postfixes) ? postfixes : postfixes[lg] ?? [] | ||||
|                 ).map((s) => new RegExp(s + "$", "i")) | ||||
|                 let clipped = searchFor | ||||
| 
 | ||||
|                 for (const postfix of postfixesUnwrapped) { | ||||
|                     const match = searchFor.match(postfix) | ||||
|                     if (match !== null) { | ||||
|                         clipped = searchFor.substring(0, searchFor.length - match[0].length) | ||||
|                         break | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 for (const prefix of prefixesUnrwapped) { | ||||
|                     const match = searchFor.match(prefix) | ||||
|                     if (match !== null) { | ||||
|                         clipped = searchFor.substring(match[0].length) | ||||
|                         break | ||||
|                     } | ||||
|                 } | ||||
|                 return clipped | ||||
|             }) | ||||
| 
 | ||||
|             defaultValueCandidate.addCallbackAndRun((clipped) => searchForValue.setData(clipped)) | ||||
|         } | ||||
| 
 | ||||
|         let instanceOf: number[] = Utils.NoNull( | ||||
|             (options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)) | ||||
|         ) | ||||
|         let notInstanceOf: number[] = Utils.NoNull( | ||||
|             (options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i)) | ||||
|         ) | ||||
| 
 | ||||
|         return new WikidataSearchBox({ | ||||
|             value: currentValue, | ||||
|             searchText: searchForValue, | ||||
|             instanceOf, | ||||
|             notInstanceOf, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | @ -43,7 +43,7 @@ export class MapLibreAdaptor implements MapProperties { | |||
|      */ | ||||
|     private _currentRasterLayer: string | ||||
| 
 | ||||
|     constructor(maplibreMap: Store<MLMap>, state?: Partial<Omit<MapProperties, "bounds">>) { | ||||
|     constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>) { | ||||
|         this._maplibreMap = maplibreMap | ||||
| 
 | ||||
|         this.location = state?.location ?? new UIEventSource({ lon: 0, lat: 0 }) | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ | |||
|    */ | ||||
|   export let map:  Writable<MaplibreMap> | ||||
| 
 | ||||
|   export let attribution = true | ||||
|   let center = {}; | ||||
| 
 | ||||
|   onMount(() => { | ||||
|  | @ -28,6 +29,9 @@ | |||
| <main> | ||||
|   <Map bind:center={center} | ||||
|        bind:map={$map} | ||||
|        {attribution} | ||||
|        css="./maplibre-gl.css" | ||||
|         | ||||
|        id="map" location={{lng: 0, lat: 0, zoom: 0}} maxzoom=24 style={styleUrl} /> | ||||
| </main> | ||||
| 
 | ||||
|  |  | |||
|  | @ -106,7 +106,7 @@ class PointRenderingLayer { | |||
|             store = new ImmutableStore(<OsmTags>feature.properties) | ||||
|         } | ||||
|         const { html, iconAnchor } = this._config.RenderIcon(store, true) | ||||
|         html.SetClass("marker") | ||||
|         html.SetClass("marker cursor-pointer") | ||||
|         const el = html.ConstructElement() | ||||
| 
 | ||||
|         if (this._onClick) { | ||||
|  | @ -244,7 +244,7 @@ class LineRenderingLayer { | |||
|                 }, | ||||
|             }) | ||||
| 
 | ||||
|             this._visibility.addCallbackAndRunD((visible) => { | ||||
|             this._visibility?.addCallbackAndRunD((visible) => { | ||||
|                 map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none") | ||||
|                 map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none") | ||||
|             }) | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import FeatureSource from "../../Logic/FeatureSource/FeatureSource" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { OsmTags } from "../../Models/OsmFeature" | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
| import { Feature } from "geojson" | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,12 +1,13 @@ | |||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" | ||||
| import Histogram from "../BigComponents/Histogram" | ||||
| import { Feature } from "geojson" | ||||
| 
 | ||||
| export class HistogramViz implements SpecialVisualization { | ||||
|     funcName = "histogram" | ||||
|     docs = "Create a histogram for a list of given values, read from the properties." | ||||
|     example = | ||||
|         "`{histogram('some_key')}` with properties being `{some_key: ['a','b','a','c']} to create a histogram" | ||||
|         '`{histogram(\'some_key\')}` with properties being `{some_key: ["a","b","a","c"]} to create a histogram' | ||||
|     args = [ | ||||
|         { | ||||
|             name: "key", | ||||
|  | @ -29,6 +30,22 @@ export class HistogramViz implements SpecialVisualization { | |||
|         }, | ||||
|     ] | ||||
| 
 | ||||
|     structuredExamples(): { feature: Feature; args: string[] }[] { | ||||
|         return [ | ||||
|             { | ||||
|                 feature: <Feature>{ | ||||
|                     type: "Feature", | ||||
|                     properties: { values: `["a","b","a","b","b","b","c","c","c","d","d"]` }, | ||||
|                     geometry: { | ||||
|                         type: "Point", | ||||
|                         coordinates: [0, 0], | ||||
|                     }, | ||||
|                 }, | ||||
|                 args: ["values"], | ||||
|             }, | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     constr( | ||||
|         state: SpecialVisualizationState, | ||||
|         tagSource: UIEventSource<Record<string, string>>, | ||||
|  |  | |||
|  | @ -5,10 +5,7 @@ import { Feature } from "geojson" | |||
| import { MapLibreAdaptor } from "../Map/MapLibreAdaptor" | ||||
| import SvelteUIElement from "../Base/SvelteUIElement" | ||||
| import MaplibreMap from "../Map/MaplibreMap.svelte" | ||||
| import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter" | ||||
| import FilteredLayer from "../../Models/FilteredLayer" | ||||
| import ShowDataLayer from "../Map/ShowDataLayer" | ||||
| import { stat } from "fs" | ||||
| 
 | ||||
| export class MinimapViz implements SpecialVisualization { | ||||
|     funcName = "minimap" | ||||
|  |  | |||
|  | @ -54,11 +54,6 @@ export default class MoveWizard extends Toggle { | |||
|         options: MoveConfig | ||||
|     ) { | ||||
|         const t = Translations.t.move | ||||
|         const loginButton = new Toggle( | ||||
|             t.loginToMove.SetClass("btn").onClick(() => state.osmConnection.AttemptLogin()), | ||||
|             undefined, | ||||
|             state.featureSwitchUserbadge | ||||
|         ) | ||||
| 
 | ||||
|         const reasons: MoveReason[] = [] | ||||
|         if (options.enableRelocation) { | ||||
|  |  | |||
							
								
								
									
										34
									
								
								UI/Popup/SpecialTranslation.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								UI/Popup/SpecialTranslation.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | |||
| <script lang="ts"> | ||||
|   import { Translation } from "../i18n/Translation"; | ||||
|   import SpecialVisualizations from "../SpecialVisualizations"; | ||||
|   import { onDestroy } from "svelte"; | ||||
|   import Locale from "../i18n/Locale"; | ||||
|   import type { RenderingSpecification, SpecialVisualizationState } from "../SpecialVisualization"; | ||||
|   import { Utils } from "../../Utils.js"; | ||||
|   import type { Feature } from "geojson"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource.js"; | ||||
|   import ToSvelte from "../Base/ToSvelte.svelte"; | ||||
|   import FromHtml from "../Base/FromHtml.svelte"; | ||||
| 
 | ||||
|   /** | ||||
|    * The 'specialTranslation' renders a `Translation`-object, but interprets the special values as well | ||||
|    */ | ||||
|   export let t: Translation; | ||||
|   export let state: SpecialVisualizationState; | ||||
|   export let tags: UIEventSource<Record<string, string>>; | ||||
|   export let feature: Feature; | ||||
|   let txt: string; | ||||
|   onDestroy(Locale.language.addCallbackAndRunD(l => { | ||||
|     txt = t.textFor(l); | ||||
|   })); | ||||
|   let specs: RenderingSpecification[]; | ||||
|   specs = SpecialVisualizations.constructSpecification(txt); | ||||
| </script> | ||||
| 
 | ||||
| {#each specs as specpart} | ||||
|   {#if typeof specpart === "string"} | ||||
|    <FromHtml src= {Utils.SubstituteKeys(specpart, $tags)}></FromHtml> | ||||
|   {:else if $tags !== undefined } | ||||
|     <ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature)}></ToSvelte> | ||||
|   {/if} | ||||
| {/each} | ||||
|  | @ -0,0 +1,34 @@ | |||
| <script lang="ts"> | ||||
|   import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"; | ||||
|   import { Utils } from "../../Utils"; | ||||
|   import { Translation } from "../i18n/Translation"; | ||||
|   import TagRenderingMapping from "./TagRenderingMapping.svelte"; | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
|   import type { Feature } from "geojson"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
|   import { onDestroy } from "svelte"; | ||||
| 
 | ||||
|   export let tags: UIEventSource<Record<string, string> | undefined>; | ||||
|   let _tags : Record<string, string> | ||||
|   onDestroy(tags.addCallbackAndRun(tags => { | ||||
|     _tags = tags | ||||
|   })) | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let selectedElement: Feature | ||||
|   export let config: TagRenderingConfig; | ||||
|   let trs: { then: Translation; icon?: string; iconClass?: string }[]; | ||||
|   $: trs = Utils.NoNull(config?.GetRenderValues(_tags)); | ||||
| </script> | ||||
| 
 | ||||
| {#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(tags))} | ||||
|   <div> | ||||
|     {#if trs.length === 1} | ||||
|       <TagRenderingMapping mapping={trs[0]} {tags} {state} feature={selectedElement}></TagRenderingMapping> | ||||
|     {/if} | ||||
|     {#if trs.length > 1} | ||||
|       {#each trs as mapping} | ||||
|         <TagRenderingMapping mapping={trs} {tags} {state} feature=""{selectedElement}></TagRenderingMapping> | ||||
|       {/each} | ||||
|     {/if} | ||||
|   </div> | ||||
| {/if} | ||||
|  | @ -6,7 +6,7 @@ import { SubstitutedTranslation } from "../SubstitutedTranslation" | |||
| import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" | ||||
| import Combine from "../Base/Combine" | ||||
| import Img from "../Base/Img" | ||||
| import { SpecialVisualisationState } from "../SpecialVisualization" | ||||
| import { SpecialVisualizationState } from "../SpecialVisualization" | ||||
| 
 | ||||
| /*** | ||||
|  * Displays the correct value for a known tagrendering | ||||
|  | @ -15,7 +15,7 @@ export default class TagRenderingAnswer extends VariableUiElement { | |||
|     constructor( | ||||
|         tagsSource: UIEventSource<any>, | ||||
|         configuration: TagRenderingConfig, | ||||
|         state: SpecialVisualisationState, | ||||
|         state: SpecialVisualizationState, | ||||
|         contentClasses: string = "", | ||||
|         contentStyle: string = "", | ||||
|         options?: { | ||||
|  |  | |||
							
								
								
									
										32
									
								
								UI/Popup/TagRenderingMapping.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								UI/Popup/TagRenderingMapping.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| <script lang="ts"> | ||||
|   import { Translation } from "../i18n/Translation"; | ||||
|   import SpecialTranslation from "./SpecialTranslation.svelte"; | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
|   import type { Feature } from "geojson"; | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource"; | ||||
| 
 | ||||
|   export let selectedElement: Feature | ||||
|   export let tags: UIEventSource<Record<string, string>>; | ||||
|   export let state: SpecialVisualizationState | ||||
|   export let mapping: { | ||||
|     then: Translation; icon?: string; iconClass?: | "small" | ||||
|       | "medium" | ||||
|       | "large" | ||||
|       | "small-height" | ||||
|       | "medium-height" | ||||
|       | "large-height" | ||||
|   }; | ||||
|   let iconclass = "mapping-icon-" + mapping.iconClass; | ||||
| 
 | ||||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| {#if mapping.icon !== undefined} | ||||
|   <div class="flex"> | ||||
|     <img class={iconclass+" mr-1"} src={mapping.icon}> | ||||
|     <SpecialTranslation t={mapping.then} {tags} {state} feature={selectedElement}></SpecialTranslation> | ||||
|   </div> | ||||
| {:else if mapping.then !== undefined} | ||||
|   <SpecialTranslation t={mapping.then} {tags} {state} feature={selectedElement}></SpecialTranslation> | ||||
| {/if} | ||||
| 
 | ||||
|  | @ -1,7 +1,6 @@ | |||
| import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import Combine from "../Base/Combine" | ||||
| import { InputElement, ReadonlyInputElement } from "../Input/InputElement" | ||||
| import ValidatedTextField from "../Input/ValidatedTextField" | ||||
| import { FixedInputElement } from "../Input/FixedInputElement" | ||||
| import { RadioButton } from "../Input/RadioButton" | ||||
| import { Utils } from "../../Utils" | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ import BaseUIElement from "../BaseUIElement" | |||
| import Img from "../Base/Img" | ||||
| import { Review } from "mangrove-reviews-typescript" | ||||
| import { Store } from "../../Logic/UIEventSource" | ||||
| import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox" | ||||
| 
 | ||||
| export default class SingleReview extends Combine { | ||||
|     constructor(review: Review & { madeByLoggedInUser: Store<boolean> }) { | ||||
|  |  | |||
|  | @ -2,17 +2,13 @@ import { Store, UIEventSource } from "../Logic/UIEventSource" | |||
| import BaseUIElement from "./BaseUIElement" | ||||
| import { DefaultGuiState } from "./DefaultGuiState" | ||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" | ||||
| import FeatureSource, { | ||||
|     IndexedFeatureSource, | ||||
|     WritableFeatureSource, | ||||
| } from "../Logic/FeatureSource/FeatureSource" | ||||
| import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource" | ||||
| import { OsmConnection } from "../Logic/Osm/OsmConnection" | ||||
| import { Changes } from "../Logic/Osm/Changes" | ||||
| import { MapProperties } from "../Models/MapProperties" | ||||
| import LayerState from "../Logic/State/LayerState" | ||||
| import { Feature } from "geojson" | ||||
| import { Feature, Geometry } from "geojson" | ||||
| import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" | ||||
| import UserRelatedState from "../Logic/State/UserRelatedState" | ||||
| import { MangroveIdentity } from "../Logic/Web/MangroveReviews" | ||||
| import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore" | ||||
| 
 | ||||
|  | @ -58,6 +54,8 @@ export interface SpecialVisualization { | |||
|     funcName: string | ||||
|     docs: string | BaseUIElement | ||||
|     example?: string | ||||
| 
 | ||||
|     structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[] | ||||
|     args: { name: string; defaultValue?: string; doc: string; required?: false | boolean }[] | ||||
|     getLayerDependencies?: (argument: string[]) => string[] | ||||
| 
 | ||||
|  | @ -68,3 +66,11 @@ export interface SpecialVisualization { | |||
|         feature: Feature | ||||
|     ): BaseUIElement | ||||
| } | ||||
| 
 | ||||
| export type RenderingSpecification = | ||||
|     | string | ||||
|     | { | ||||
|           func: SpecialVisualization | ||||
|           args: string[] | ||||
|           style: string | ||||
|       } | ||||
|  |  | |||
|  | @ -3,7 +3,11 @@ import { FixedUiElement } from "./Base/FixedUiElement" | |||
| import BaseUIElement from "./BaseUIElement" | ||||
| import Title from "./Base/Title" | ||||
| import Table from "./Base/Table" | ||||
| import { SpecialVisualization } from "./SpecialVisualization" | ||||
| import { | ||||
|     RenderingSpecification, | ||||
|     SpecialVisualization, | ||||
|     SpecialVisualizationState, | ||||
| } from "./SpecialVisualization" | ||||
| import { HistogramViz } from "./Popup/HistogramViz" | ||||
| import { StealViz } from "./Popup/StealViz" | ||||
| import { MinimapViz } from "./Popup/MinimapViz" | ||||
|  | @ -51,10 +55,97 @@ import FeatureReviews from "../Logic/Web/MangroveReviews" | |||
| import Maproulette from "../Logic/Maproulette" | ||||
| import SvelteUIElement from "./Base/SvelteUIElement" | ||||
| import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| 
 | ||||
| export default class SpecialVisualizations { | ||||
|     public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList() | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * For a given string, returns a specification what parts are fixed and what parts are special renderings. | ||||
|      * Note that _normal_ substitutions are ignored. | ||||
|      * | ||||
|      * // Return empty list on empty input
 | ||||
|      * SubstitutedTranslation.ExtractSpecialComponents("") // => []
 | ||||
|      * | ||||
|      * // Advanced cases with commas, braces and newlines should be handled without problem
 | ||||
|      * const templates = SubstitutedTranslation.ExtractSpecialComponents("{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.osm.be/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}") | ||||
|      * const templ = templates[0] | ||||
|      * templ.special.func.funcName // => "send_email"
 | ||||
|      * templ.special.args[0] = "{email}" | ||||
|      */ | ||||
|     public static constructSpecification( | ||||
|         template: string, | ||||
|         extraMappings: SpecialVisualization[] = [] | ||||
|     ): RenderingSpecification[] { | ||||
|         if (template === "") { | ||||
|             return [] | ||||
|         } | ||||
| 
 | ||||
|         const allKnownSpecials = extraMappings.concat(SpecialVisualizations.specialVisualizations) | ||||
|         for (const knownSpecial of allKnownSpecials) { | ||||
|             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 | ||||
|             const matched = template.match( | ||||
|                 new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s") | ||||
|             ) | ||||
|             if (matched != null) { | ||||
|                 // We found a special component that should be brought to live
 | ||||
|                 const partBefore = SpecialVisualizations.constructSpecification( | ||||
|                     matched[1], | ||||
|                     extraMappings | ||||
|                 ) | ||||
|                 const argument = matched[2].trim() | ||||
|                 const style = matched[3]?.substring(1) ?? "" | ||||
|                 const partAfter = SpecialVisualizations.constructSpecification( | ||||
|                     matched[4], | ||||
|                     extraMappings | ||||
|                 ) | ||||
|                 const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "") | ||||
|                 if (argument.length > 0) { | ||||
|                     const realArgs = argument.split(",").map((str) => | ||||
|                         str | ||||
|                             .trim() | ||||
|                             .replace(/&LPARENS/g, "(") | ||||
|                             .replace(/&RPARENS/g, ")") | ||||
|                             .replace(/&LBRACE/g, "{") | ||||
|                             .replace(/&RBRACE/g, "}") | ||||
|                             .replace(/&COMMA/g, ",") | ||||
|                     ) | ||||
|                     for (let i = 0; i < realArgs.length; i++) { | ||||
|                         if (args.length <= i) { | ||||
|                             args.push(realArgs[i]) | ||||
|                         } else { | ||||
|                             args[i] = realArgs[i] | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 const element: RenderingSpecification = { | ||||
|                     args: args, | ||||
|                     style: style, | ||||
|                     func: knownSpecial, | ||||
|                 } | ||||
|                 return [...partBefore, element, ...partAfter] | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Let's to a small sanity check to help the theme designers:
 | ||||
|         if (template.search(/{[^}]+\([^}]*\)}/) >= 0) { | ||||
|             // Hmm, we might have found an invalid rendering name
 | ||||
|             console.warn( | ||||
|                 "Found a suspicious special rendering value in: ", | ||||
|                 template, | ||||
|                 " did you mean one of: " | ||||
|                 /*SpecialVisualizations.specialVisualizations | ||||
|                         .map((sp) => sp.funcName + "()") | ||||
|                         .join(", ")*/ | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         // IF we end up here, no changes have to be made - except to remove any resting {}
 | ||||
|         return [template] | ||||
|     } | ||||
| 
 | ||||
|     public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined { | ||||
|         if (typeof viz === "string") { | ||||
|             viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz) | ||||
|  | @ -649,7 +740,7 @@ export default class SpecialVisualizations { | |||
|                         defaultValue: "mr_taskId", | ||||
|                     }, | ||||
|                 ], | ||||
|                 constr: (state, tagsSource, args, guistate) => { | ||||
|                 constr: (state, tagsSource, args) => { | ||||
|                     let [message, image, message_closed, status, maproulette_id_key] = args | ||||
|                     if (image === "") { | ||||
|                         image = "confirm" | ||||
|  | @ -720,7 +811,7 @@ export default class SpecialVisualizations { | |||
|                 funcName: "statistics", | ||||
|                 docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer", | ||||
|                 args: [], | ||||
|                 constr: (state, tagsSource, args, guiState) => { | ||||
|                 constr: (state) => { | ||||
|                     return new Combine( | ||||
|                         state.layout.layers | ||||
|                             .filter((l) => l.name !== null) | ||||
|  | @ -852,4 +943,23 @@ export default class SpecialVisualizations { | |||
| 
 | ||||
|         return specialVisualizations | ||||
|     } | ||||
| 
 | ||||
|     // noinspection JSUnusedGlobalSymbols
 | ||||
|     public static renderExampleOfSpecial( | ||||
|         state: SpecialVisualizationState, | ||||
|         s: SpecialVisualization | ||||
|     ): BaseUIElement { | ||||
|         const examples = | ||||
|             s.structuredExamples === undefined | ||||
|                 ? [] | ||||
|                 : s.structuredExamples().map((e) => { | ||||
|                       return s.constr( | ||||
|                           state, | ||||
|                           new UIEventSource<Record<string, string>>(e.feature.properties), | ||||
|                           e.args, | ||||
|                           e.feature | ||||
|                       ) | ||||
|                   }) | ||||
|         return new Combine([new Title(s.funcName), s.docs, ...examples]) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -7,10 +7,10 @@ import { Utils } from "../Utils" | |||
| import { VariableUiElement } from "./Base/VariableUIElement" | ||||
| import Combine from "./Base/Combine" | ||||
| import BaseUIElement from "./BaseUIElement" | ||||
| import { DefaultGuiState } from "./DefaultGuiState" | ||||
| import FeaturePipelineState from "../Logic/State/FeaturePipelineState" | ||||
| import LinkToWeblate from "./Base/LinkToWeblate" | ||||
| import { SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization" | ||||
| import SpecialVisualizations from "./SpecialVisualizations" | ||||
| import { Feature } from "geojson" | ||||
| 
 | ||||
| export class SubstitutedTranslation extends VariableUiElement { | ||||
|     public constructor( | ||||
|  | @ -21,10 +21,10 @@ export class SubstitutedTranslation extends VariableUiElement { | |||
|             string, | ||||
|             | BaseUIElement | ||||
|             | (( | ||||
|                   state: FeaturePipelineState, | ||||
|                   state: SpecialVisualizationState, | ||||
|                   tagSource: UIEventSource<Record<string, string>>, | ||||
|                   argument: string[], | ||||
|                   guistate: DefaultGuiState | ||||
|                   feature: Feature | ||||
|               ) => BaseUIElement) | ||||
|         > = undefined | ||||
|     ) { | ||||
|  | @ -55,19 +55,23 @@ export class SubstitutedTranslation extends VariableUiElement { | |||
|                     txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`) | ||||
|                 }) | ||||
| 
 | ||||
|                 const allElements = SubstitutedTranslation.ExtractSpecialComponents( | ||||
|                 const allElements = SpecialVisualizations.constructSpecification( | ||||
|                     txt, | ||||
|                     extraMappings | ||||
|                 ).map((proto) => { | ||||
|                     if (proto.fixed !== undefined) { | ||||
|                     if (typeof proto === "string") { | ||||
|                         if (tagsSource === undefined) { | ||||
|                             return Utils.SubstituteKeys(proto.fixed, undefined) | ||||
|                             return Utils.SubstituteKeys(proto, undefined) | ||||
|                         } | ||||
|                         return new VariableUiElement( | ||||
|                             tagsSource.map((tags) => Utils.SubstituteKeys(proto.fixed, tags)) | ||||
|                             tagsSource.map((tags) => Utils.SubstituteKeys(proto, tags)) | ||||
|                         ) | ||||
|                     } | ||||
|                     const viz = proto.special | ||||
|                     const viz: { | ||||
|                         func: SpecialVisualization | ||||
|                         args: string[] | ||||
|                         style: string | ||||
|                     } = proto | ||||
|                     if (viz === undefined) { | ||||
|                         console.error( | ||||
|                             "SPECIALRENDERING UNDEFINED for", | ||||
|  | @ -77,9 +81,12 @@ export class SubstitutedTranslation extends VariableUiElement { | |||
|                         return undefined | ||||
|                     } | ||||
|                     try { | ||||
|                         const feature = state.indexedFeatures.featuresById.data.get( | ||||
|                             tagsSource.data.id | ||||
|                         ) | ||||
|                         return viz.func | ||||
|                             .constr(state, tagsSource, proto.special.args) | ||||
|                             ?.SetStyle(proto.special.style) | ||||
|                             .constr(state, tagsSource, proto.args, feature) | ||||
|                             ?.SetStyle(proto.style) | ||||
|                     } catch (e) { | ||||
|                         console.error("SPECIALRENDERING FAILED for", tagsSource.data?.id, e) | ||||
|                         return new FixedUiElement( | ||||
|  | @ -97,98 +104,4 @@ export class SubstitutedTranslation extends VariableUiElement { | |||
| 
 | ||||
|         this.SetClass("w-full") | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * // Return empty list on empty input
 | ||||
|      * SubstitutedTranslation.ExtractSpecialComponents("") // => []
 | ||||
|      * | ||||
|      * // Advanced cases with commas, braces and newlines should be handled without problem
 | ||||
|      * const templates = SubstitutedTranslation.ExtractSpecialComponents("{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.osm.be/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}") | ||||
|      * const templ = templates[0] | ||||
|      * templ.special.func.funcName // => "send_email"
 | ||||
|      * templ.special.args[0] = "{email}" | ||||
|      */ | ||||
|     public static ExtractSpecialComponents( | ||||
|         template: string, | ||||
|         extraMappings: SpecialVisualization[] = [] | ||||
|     ): { | ||||
|         fixed?: string | ||||
|         special?: { | ||||
|             func: SpecialVisualization | ||||
|             args: string[] | ||||
|             style: string | ||||
|         } | ||||
|     }[] { | ||||
|         if (template === "") { | ||||
|             return [] | ||||
|         } | ||||
| 
 | ||||
|         for (const knownSpecial of extraMappings.concat( | ||||
|             [] // TODO enable   SpecialVisualizations.specialVisualizations
 | ||||
|         )) { | ||||
|             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 | ||||
|             const matched = template.match( | ||||
|                 new RegExp(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`, "s") | ||||
|             ) | ||||
|             if (matched != null) { | ||||
|                 // We found a special component that should be brought to live
 | ||||
|                 const partBefore = SubstitutedTranslation.ExtractSpecialComponents( | ||||
|                     matched[1], | ||||
|                     extraMappings | ||||
|                 ) | ||||
|                 const argument = matched[2].trim() | ||||
|                 const style = matched[3]?.substring(1) ?? "" | ||||
|                 const partAfter = SubstitutedTranslation.ExtractSpecialComponents( | ||||
|                     matched[4], | ||||
|                     extraMappings | ||||
|                 ) | ||||
|                 const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "") | ||||
|                 if (argument.length > 0) { | ||||
|                     const realArgs = argument.split(",").map((str) => | ||||
|                         str | ||||
|                             .trim() | ||||
|                             .replace(/&LPARENS/g, "(") | ||||
|                             .replace(/&RPARENS/g, ")") | ||||
|                             .replace(/&LBRACE/g, "{") | ||||
|                             .replace(/&RBRACE/g, "}") | ||||
|                             .replace(/&COMMA/g, ",") | ||||
|                     ) | ||||
|                     for (let i = 0; i < realArgs.length; i++) { | ||||
|                         if (args.length <= i) { | ||||
|                             args.push(realArgs[i]) | ||||
|                         } else { | ||||
|                             args[i] = realArgs[i] | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 let element | ||||
|                 element = { | ||||
|                     special: { | ||||
|                         args: args, | ||||
|                         style: style, | ||||
|                         func: knownSpecial, | ||||
|                     }, | ||||
|                 } | ||||
|                 return [...partBefore, element, ...partAfter] | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Let's to a small sanity check to help the theme designers:
 | ||||
|         if (template.search(/{[^}]+\([^}]*\)}/) >= 0) { | ||||
|             // Hmm, we might have found an invalid rendering name
 | ||||
|             console.warn( | ||||
|                 "Found a suspicious special rendering value in: ", | ||||
|                 template, | ||||
|                 " did you mean one of: " | ||||
|                 /*SpecialVisualizations.specialVisualizations | ||||
|                     .map((sp) => sp.funcName + "()") | ||||
|                     .join(", ")*/ | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         // IF we end up here, no changes have to be made - except to remove any resting {}
 | ||||
|         return [{ fixed: template }] | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ | |||
|   import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui"; | ||||
|   import Translations from "./i18n/Translations"; | ||||
|   import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import Tr from "./Base/Tr.svelte"; | ||||
| 
 | ||||
|   export let layout: LayoutConfig; | ||||
|   const state = new ThemeViewState(layout); | ||||
|  | @ -48,7 +49,7 @@ | |||
|     <div class="flex mr-2 items-center"> | ||||
|       <img class="w-8 h-8 block mr-2" src={layout.icon}> | ||||
|       <b> | ||||
|         {layout.title} | ||||
|         <Tr t={layout.title}></Tr> | ||||
|       </b> | ||||
|     </div> | ||||
|   </MapControlButton> | ||||
|  | @ -58,9 +59,7 @@ | |||
| </div> | ||||
| 
 | ||||
| <div class="absolute bottom-0 left-0 mb-4 ml-4"> | ||||
|   <MapControlButton on:click={() => state.guistate.filterViewIsOpened.setData(true)}> | ||||
|     <ToSvelte class="w-7 h-7 block" construct={Svg.layers_ui}></ToSvelte> | ||||
|   </MapControlButton> | ||||
| 
 | ||||
| </div> | ||||
| 
 | ||||
| <div class="absolute bottom-0 right-0 mb-4 mr-4"> | ||||
|  | @ -86,17 +85,6 @@ | |||
|   </If> | ||||
| </div> | ||||
| 
 | ||||
| <If condition={state.guistate.filterViewIsOpened}> | ||||
|   <div class="normal-background absolute bottom-0 left-0 flex flex-col"> | ||||
|     <div on:click={() => state.guistate.filterViewIsOpened.setData(false)}>Close</div> | ||||
|     <!-- Filter panel -- TODO move to actual location--> | ||||
|     {#each layout.layers as layer} | ||||
|       <Filterview filteredLayer={state.layerState.filteredLayers.get(layer.id)}></Filterview> | ||||
|     {/each} | ||||
| 
 | ||||
|     <RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker> | ||||
|   </div> | ||||
| </If> | ||||
| 
 | ||||
| <If condition={state.guistate.welcomeMessageIsOpened}> | ||||
|   <!-- Theme page --> | ||||
|  | @ -105,31 +93,47 @@ | |||
|       <div on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}>Close</div> | ||||
|       <TabGroup> | ||||
|         <TabList> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>About</Tab> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 2</Tab> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|             <Tr t={layout.title}/> | ||||
|           </Tab> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}> | ||||
|             <Tr t={Translations.t.general.menu.filter}/> | ||||
|           </Tab> | ||||
|           <Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 3</Tab> | ||||
|         </TabList> | ||||
|         <TabPanels> | ||||
|           <TabPanel class="flex flex-col"> | ||||
|             <ToSvelte construct={() => layout.description}></ToSvelte> | ||||
|             {Translations.t.general.welcomeExplanation.general} | ||||
|             <Tr t={layout.description}></Tr> | ||||
|             <Tr t={Translations.t.general.welcomeExplanation.general}/> | ||||
|             {#if layout.layers.some((l) => l.presets?.length > 0)} | ||||
|               <If condition={state.featureSwitches.featureSwitchAddNew}> | ||||
|                 {Translations.t.general.welcomeExplanation.addNew} | ||||
|              <Tr t={Translations.t.general.welcomeExplanation.addNew}/> | ||||
|               </If> | ||||
|             {/if} | ||||
| 
 | ||||
|             <!--toTheMap, | ||||
|             loginStatus.SetClass("block mt-6 pt-2 md:border-t-2 border-dotted border-gray-400"), | ||||
|             --> | ||||
|             <ToSvelte construct= {() =>  layout.descriptionTail}></ToSvelte> | ||||
|             <Tr t={layout.descriptionTail}></Tr> | ||||
|             <div class="m-x-8"> | ||||
|             <button class="subtle-background rounded w-full p-4">Explore the map</button> | ||||
|               <button class="subtle-background rounded w-full p-4" | ||||
|                       on:click={() => state.guistate.welcomeMessageIsOpened.setData(false)}> | ||||
|                 <Tr t={Translations.t.general.openTheMap} /> | ||||
|               </button> | ||||
|             </div> | ||||
| 
 | ||||
| 
 | ||||
|           </TabPanel> | ||||
|           <TabPanel>Content 2</TabPanel> | ||||
|           <TabPanel> | ||||
|             <div class="flex flex-col"> | ||||
|               <!-- Filter panel -- TODO move to actual location--> | ||||
|               {#each layout.layers as layer} | ||||
|                 <Filterview filteredLayer={state.layerState.filteredLayers.get(layer.id)}></Filterview> | ||||
|               {/each} | ||||
| 
 | ||||
|               <RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker> | ||||
|             </div> | ||||
|           </TabPanel> | ||||
|           <TabPanel>Content 3</TabPanel> | ||||
|         </TabPanels> | ||||
|       </TabGroup> | ||||
|  | @ -163,15 +167,14 @@ | |||
|   </div> | ||||
| </If> | ||||
| 
 | ||||
| <If condition={selectedElement}> | ||||
| {#if $selectedElement !== undefined && $selectedLayer !== undefined} | ||||
|   <div class="absolute top-0 right-0 normal-background"> | ||||
| 
 | ||||
|     <SelectedElementView layer={selectedLayer} {selectedElement} | ||||
|                          tags={selectedElementTags}></SelectedElementView> | ||||
|     <SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement} | ||||
|                          tags={$selectedElementTags} state={state}></SelectedElementView> | ||||
| 
 | ||||
|   </div> | ||||
| </If> | ||||
| 
 | ||||
| {/if} | ||||
| <style> | ||||
|     /* WARNING: This is just for demonstration. | ||||
|         Using :global() in this way can be risky. */ | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { VariableUiElement } from "../Base/VariableUIElement" | ||||
| import { Store, UIEventSource } from "../../Logic/UIEventSource" | ||||
| import { Store } from "../../Logic/UIEventSource" | ||||
| import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata" | ||||
| import { Translation, TypedTranslation } from "../i18n/Translation" | ||||
| import { FixedUiElement } from "../Base/FixedUiElement" | ||||
|  |  | |||
|  | @ -51,10 +51,6 @@ export class Translation extends BaseUIElement { | |||
|         return this.textFor(Translation.forcedLanguage ?? Locale.language.data) | ||||
|     } | ||||
| 
 | ||||
|     public toString() { | ||||
|         return this.txt | ||||
|     } | ||||
| 
 | ||||
|     static ExtractAllTranslationsFrom( | ||||
|         object: any, | ||||
|         context = "" | ||||
|  | @ -91,6 +87,10 @@ export class Translation extends BaseUIElement { | |||
|         return new Translation(translations) | ||||
|     } | ||||
| 
 | ||||
|     public toString() { | ||||
|         return this.txt | ||||
|     } | ||||
| 
 | ||||
|     Destroy() { | ||||
|         super.Destroy() | ||||
|         this.isDestroyed = true | ||||
|  |  | |||
|  | @ -1,9 +1,6 @@ | |||
| import { Utils } from "./Utils" | ||||
| import AllThemesGui from "./UI/AllThemesGui" | ||||
| import { QueryParameters } from "./Logic/Web/QueryParameters" | ||||
| import StatisticsGUI from "./UI/StatisticsGUI" | ||||
| import { FixedUiElement } from "./UI/Base/FixedUiElement" | ||||
| import { PdfExportGui } from "./UI/BigComponents/PdfExportGui" | ||||
| 
 | ||||
| const layout = QueryParameters.GetQueryParameter("layout", undefined).data ?? "" | ||||
| const customLayout = QueryParameters.GetQueryParameter("userlayout", undefined).data ?? "" | ||||
|  | @ -32,23 +29,4 @@ if (layout !== "") { | |||
| } | ||||
| 
 | ||||
| Utils.DisableLongPresses() | ||||
| document.getElementById("decoration-desktop").remove() | ||||
| const mode = QueryParameters.GetQueryParameter( | ||||
|     "mode", | ||||
|     "map", | ||||
|     "The mode the application starts in, e.g. 'statistics'" | ||||
| ) | ||||
| 
 | ||||
| if (mode.data === "statistics") { | ||||
|     console.log("Statistics mode!") | ||||
|     new FixedUiElement("").AttachTo("centermessage") | ||||
|     new StatisticsGUI().SetClass("w-full h-full pointer-events-auto").AttachTo("topleft-tools") | ||||
| } else if (mode.data === "pdf") { | ||||
|     new FixedUiElement("").AttachTo("centermessage") | ||||
|     const div = document.createElement("div") | ||||
|     div.id = "extra_div_for_maps" | ||||
|     new PdfExportGui(div.id).SetClass("pointer-events-auto").AttachTo("topleft-tools") | ||||
|     document.getElementById("topleft-tools").appendChild(div) | ||||
| } else { | ||||
|     new AllThemesGui().setup() | ||||
| } | ||||
| new AllThemesGui().setup() | ||||
|  |  | |||
|  | @ -324,7 +324,6 @@ | |||
|           "hideInAnswer": true | ||||
|         } | ||||
|       ] | ||||
|      | ||||
|     }, | ||||
|     { | ||||
|       "id": "max_bolts", | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
|   "description": { | ||||
|     "en": "A layer acting as library for icon-tagrenderings, especially to show as badge next to a POI" | ||||
|   }, | ||||
|   "source":"special:library", | ||||
|   "source": "special:library", | ||||
|   "title": null, | ||||
|   "tagRenderings": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
|   "id": "import_candidate", | ||||
|   "description": "Layer used as template in the importHelper", | ||||
|   "source":"special", | ||||
|   "source": "special", | ||||
|   "mapRendering": [ | ||||
|     { | ||||
|       "location": [ | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| { | ||||
|   "id": "matchpoint", | ||||
|   "description": "The default rendering for a locationInput which snaps onto another object", | ||||
|   "source":"special", | ||||
|   "source": "special", | ||||
|   "mapRendering": [ | ||||
|     { | ||||
|       "location": [ | ||||
|  |  | |||
|  | @ -1183,6 +1183,10 @@ video { | |||
|   width: auto; | ||||
| } | ||||
| 
 | ||||
| .w-48 { | ||||
|   width: 12rem; | ||||
| } | ||||
| 
 | ||||
| .min-w-min { | ||||
|   min-width: -webkit-min-content; | ||||
|   min-width: min-content; | ||||
|  | @ -1509,6 +1513,11 @@ video { | |||
|   border-color: rgb(107 114 128 / var(--tw-border-opacity)); | ||||
| } | ||||
| 
 | ||||
| .border-red-500 { | ||||
|   --tw-border-opacity: 1; | ||||
|   border-color: rgb(239 68 68 / var(--tw-border-opacity)); | ||||
| } | ||||
| 
 | ||||
| .border-opacity-50 { | ||||
|   --tw-border-opacity: 0.5; | ||||
| } | ||||
|  | @ -1580,10 +1589,6 @@ video { | |||
|   padding: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .p-8 { | ||||
|   padding: 2rem; | ||||
| } | ||||
| 
 | ||||
| .p-3 { | ||||
|   padding: 0.75rem; | ||||
| } | ||||
|  | @ -1596,6 +1601,10 @@ video { | |||
|   padding: 0.125rem; | ||||
| } | ||||
| 
 | ||||
| .p-8 { | ||||
|   padding: 2rem; | ||||
| } | ||||
| 
 | ||||
| .py-4 { | ||||
|   padding-top: 1rem; | ||||
|   padding-bottom: 1rem; | ||||
|  | @ -1921,6 +1930,10 @@ video { | |||
|   transition-duration: 150ms; | ||||
| } | ||||
| 
 | ||||
| .ease-in-out { | ||||
|   transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | ||||
| } | ||||
| 
 | ||||
| .z-above-map { | ||||
|   z-index: 10000; | ||||
| } | ||||
|  | @ -1971,8 +1984,6 @@ video { | |||
|   --shadow-color: #00000066; | ||||
|   --return-to-the-map-height: 2em; | ||||
|   --image-carousel-height: 350px; | ||||
|   /* The border colour of the leaflet popup */ | ||||
|   --popup-border: white; | ||||
|   /* Technical variable to make some dynamic behaviour possible; set by javascript. */ | ||||
|   --variable-title-height: 0px; | ||||
| } | ||||
|  | @ -1989,31 +2000,6 @@ body { | |||
|   font-family: "Helvetica Neue", Arial, sans-serif; | ||||
| } | ||||
| 
 | ||||
| .leaflet-overlay-pane .leaflet-zoom-animated { | ||||
|   /* Another workaround to keep leaflet working */ | ||||
|   width: initial !important; | ||||
|   height: initial !important; | ||||
|   box-sizing: initial !important; | ||||
| } | ||||
| 
 | ||||
| .leaflet-marker-icon img { | ||||
|   -webkit-touch-callout: none; | ||||
|   /* prevent callout to copy image, etc when tap to hold */ | ||||
| } | ||||
| 
 | ||||
| .leaflet-control-attribution { | ||||
|   display: flex; | ||||
| } | ||||
| 
 | ||||
| .badge { | ||||
| } | ||||
| 
 | ||||
| .badge svg { | ||||
|   /*Workaround for leaflet*/ | ||||
|   width: unset !important; | ||||
|   height: 100% !important; | ||||
| } | ||||
| 
 | ||||
| svg, | ||||
| img { | ||||
|   box-sizing: content-box; | ||||
|  | @ -2255,28 +2241,6 @@ li::marker { | |||
|   fill: var(--catch-detail-color) !important; | ||||
| } | ||||
| 
 | ||||
| #leafletDiv { | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| .leaflet-popup-content-wrapper { | ||||
|   background-color: var(--background-color); | ||||
|   color: var(--foreground-color); | ||||
|   border: 2px solid var(--popup-border); | ||||
|   box-shadow: 0 3px 14px var(--shadow-color) !important; | ||||
| } | ||||
| 
 | ||||
| .leaflet-container { | ||||
|   font: unset !important; | ||||
|   background-color: var(--background-color) !important; | ||||
| } | ||||
| 
 | ||||
| .leaflet-popup-tip { | ||||
|   background-color: var(--popup-border) !important; | ||||
|   color: var(--popup-border) !important; | ||||
|   box-shadow: 0 3px 14px var(--shadow-color) !important; | ||||
| } | ||||
| 
 | ||||
| .single-layer-selection-toggle { | ||||
|   position: relative; | ||||
|   width: 2em; | ||||
|  | @ -2408,131 +2372,17 @@ li::marker { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .hand-drag-animation { | ||||
|   -webkit-animation: hand-drag-animation 6s ease-in-out infinite; | ||||
|           animation: hand-drag-animation 6s ease-in-out infinite; | ||||
|   -webkit-transform-origin: 50% 125%; | ||||
|           transform-origin: 50% 125%; | ||||
| } | ||||
| 
 | ||||
| @-webkit-keyframes hand-drag-animation { | ||||
|   /* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */ | ||||
| 
 | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|     -webkit-transform: rotate(-30deg); | ||||
|             transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   6% { | ||||
|     opacity: 1; | ||||
|     -webkit-transform: rotate(-30deg); | ||||
|             transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   12% { | ||||
|     opacity: 1; | ||||
|     -webkit-transform: rotate(-45deg); | ||||
|             transform: rotate(-45deg); | ||||
|   } | ||||
| 
 | ||||
|   24% { | ||||
|     opacity: 1; | ||||
|     -webkit-transform: rotate(-00deg); | ||||
|             transform: rotate(-00deg); | ||||
|   } | ||||
| 
 | ||||
|   30% { | ||||
|     opacity: 1; | ||||
|     -webkit-transform: rotate(-30deg); | ||||
|             transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   36% { | ||||
|     opacity: 0; | ||||
|     -webkit-transform: rotate(-30deg); | ||||
|             transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
|     opacity: 0; | ||||
|     -webkit-transform: rotate(-30deg); | ||||
|             transform: rotate(-30deg); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @keyframes hand-drag-animation { | ||||
|   /* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */ | ||||
| 
 | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|     -webkit-transform: rotate(-30deg); | ||||
|             transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   6% { | ||||
|     opacity: 1; | ||||
|     -webkit-transform: rotate(-30deg); | ||||
|             transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   12% { | ||||
|     opacity: 1; | ||||
|     -webkit-transform: rotate(-45deg); | ||||
|             transform: rotate(-45deg); | ||||
|   } | ||||
| 
 | ||||
|   24% { | ||||
|     opacity: 1; | ||||
|     -webkit-transform: rotate(-00deg); | ||||
|             transform: rotate(-00deg); | ||||
|   } | ||||
| 
 | ||||
|   30% { | ||||
|     opacity: 1; | ||||
|     -webkit-transform: rotate(-30deg); | ||||
|             transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   36% { | ||||
|     opacity: 0; | ||||
|     -webkit-transform: rotate(-30deg); | ||||
|             transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
|     opacity: 0; | ||||
|     -webkit-transform: rotate(-30deg); | ||||
|             transform: rotate(-30deg); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /***************** Info box (box containing features and questions ******************/ | ||||
| 
 | ||||
| input { | ||||
|   color: var(--foreground-color); | ||||
| } | ||||
| 
 | ||||
| .leaflet-popup-content { | ||||
|   width: 45em !important; | ||||
|   margin: 0.25rem !important; | ||||
| } | ||||
| 
 | ||||
| .leaflet-div-icon { | ||||
|   background-color: unset !important; | ||||
|   border: unset !important; | ||||
| } | ||||
| 
 | ||||
| .floating-element-width { | ||||
|   max-width: calc(100vw - 5em); | ||||
|   width: 40em; | ||||
| } | ||||
| 
 | ||||
| .leaflet-div-icon svg { | ||||
|   width: calc(100%); | ||||
|   height: calc(100%); | ||||
| } | ||||
| 
 | ||||
| /****** ShareScreen *****/ | ||||
| 
 | ||||
| .literal-code { | ||||
|  |  | |||
							
								
								
									
										103
									
								
								index.css
									
										
									
									
									
								
							
							
						
						
									
										103
									
								
								index.css
									
										
									
									
									
								
							|  | @ -87,9 +87,6 @@ | |||
|   --return-to-the-map-height: 2em; | ||||
|   --image-carousel-height: 350px; | ||||
| 
 | ||||
|   /* The border colour of the leaflet popup */ | ||||
|   --popup-border: white; | ||||
| 
 | ||||
|   /* Technical variable to make some dynamic behaviour possible; set by javascript. */ | ||||
|   --variable-title-height: 0px; | ||||
| } | ||||
|  | @ -106,29 +103,6 @@ body { | |||
|   font-family: "Helvetica Neue", Arial, sans-serif; | ||||
| } | ||||
| 
 | ||||
| .leaflet-overlay-pane .leaflet-zoom-animated { | ||||
|   /* Another workaround to keep leaflet working */ | ||||
|   width: initial !important; | ||||
|   height: initial !important; | ||||
|   box-sizing: initial !important; | ||||
| } | ||||
| 
 | ||||
| .leaflet-marker-icon img { | ||||
|   -webkit-touch-callout: none;                /* prevent callout to copy image, etc when tap to hold */ | ||||
| } | ||||
| 
 | ||||
| .leaflet-control-attribution { | ||||
|   display: flex; | ||||
| } | ||||
| 
 | ||||
| .badge { | ||||
| } | ||||
| 
 | ||||
| .badge svg { | ||||
|   /*Workaround for leaflet*/ | ||||
|   width: unset !important; | ||||
|   height: 100% !important; | ||||
| } | ||||
| 
 | ||||
| svg, | ||||
| img { | ||||
|  | @ -365,27 +339,6 @@ li::marker { | |||
|   fill: var(--catch-detail-color) !important; | ||||
| } | ||||
| 
 | ||||
| #leafletDiv { | ||||
|   height: 100%; | ||||
| } | ||||
| 
 | ||||
| .leaflet-popup-content-wrapper { | ||||
|   background-color: var(--background-color); | ||||
|   color: var(--foreground-color); | ||||
|   border: 2px solid var(--popup-border); | ||||
|   box-shadow: 0 3px 14px var(--shadow-color) !important; | ||||
| } | ||||
| 
 | ||||
| .leaflet-container { | ||||
|   font: unset !important; | ||||
|   background-color: var(--background-color) !important; | ||||
| } | ||||
| 
 | ||||
| .leaflet-popup-tip { | ||||
|   background-color: var(--popup-border) !important; | ||||
|   color: var(--popup-border) !important; | ||||
|   box-shadow: 0 3px 14px var(--shadow-color) !important; | ||||
| } | ||||
| 
 | ||||
| .single-layer-selection-toggle { | ||||
|   position: relative; | ||||
|  | @ -499,48 +452,6 @@ li::marker { | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .hand-drag-animation { | ||||
|   animation: hand-drag-animation 6s ease-in-out infinite; | ||||
|   transform-origin: 50% 125%; | ||||
| } | ||||
| 
 | ||||
| @keyframes hand-drag-animation { | ||||
|   /* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */ | ||||
|   0% { | ||||
|     opacity: 0; | ||||
|     transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   6% { | ||||
|     opacity: 1; | ||||
|     transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   12% { | ||||
|     opacity: 1; | ||||
|     transform: rotate(-45deg); | ||||
|   } | ||||
| 
 | ||||
|   24% { | ||||
|     opacity: 1; | ||||
|     transform: rotate(-00deg); | ||||
|   } | ||||
| 
 | ||||
|   30% { | ||||
|     opacity: 1; | ||||
|     transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   36% { | ||||
|     opacity: 0; | ||||
|     transform: rotate(-30deg); | ||||
|   } | ||||
| 
 | ||||
|   100% { | ||||
|     opacity: 0; | ||||
|     transform: rotate(-30deg); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| /***************** Info box (box containing features and questions ******************/ | ||||
|  | @ -549,25 +460,11 @@ input { | |||
|   color: var(--foreground-color); | ||||
| } | ||||
| 
 | ||||
| .leaflet-popup-content { | ||||
|   width: 45em !important; | ||||
|   margin: 0.25rem !important; | ||||
| } | ||||
| 
 | ||||
| .leaflet-div-icon { | ||||
|   background-color: unset !important; | ||||
|   border: unset !important; | ||||
| } | ||||
| 
 | ||||
| .floating-element-width { | ||||
|   max-width: calc(100vw - 5em); | ||||
|   width: 40em; | ||||
| } | ||||
| 
 | ||||
| .leaflet-div-icon svg { | ||||
|   width: calc(100%); | ||||
|   height: calc(100%); | ||||
| } | ||||
| 
 | ||||
| /****** ShareScreen *****/ | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										16
									
								
								index.html
									
										
									
									
									
								
							
							
						
						
									
										16
									
								
								index.html
									
										
									
									
									
								
							|  | @ -51,22 +51,8 @@ | |||
| </head> | ||||
| <body> | ||||
| 
 | ||||
| <div id="decoration-desktop" style="position: fixed; left: 1em; bottom: 1em; width:35vh; height:35vh;"> | ||||
|     <!-- A nice decoration while loading or on errors --> | ||||
|     <!-- DECORATION 0 START --> | ||||
|     <img src="./assets/svg/add.svg"/> | ||||
|     <!-- DECORATION 0 END --> | ||||
| </div> | ||||
| 
 | ||||
| <div id="top-left"> | ||||
| </div> | ||||
| 
 | ||||
| <div class="clutter absolute h-24 left-24 right-24 top-56 text-xl text-center" | ||||
|      id="centermessage" style="z-index: 4000"> | ||||
|     Loading MapComplete, hang on... | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| <div id="main"></div> | ||||
| <script type="module" src="./all_themes_index.ts"></script> | ||||
| <script async data-goatcounter="https://pietervdvn.goatcounter.com/count" src="//gc.zgo.at/count.js"></script> | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										1
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										1
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -4,7 +4,6 @@ import { Utils } from "./Utils" | |||
| import AllThemesGui from "./UI/AllThemesGui" | ||||
| import DetermineLayout from "./Logic/DetermineLayout" | ||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig" | ||||
| import DefaultGUI from "./UI/DefaultGUI" | ||||
| import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation" | ||||
| import { DefaultGuiState } from "./UI/DefaultGuiState" | ||||
| 
 | ||||
|  |  | |||
|  | @ -88,7 +88,7 @@ | |||
|     }, | ||||
|     "general": { | ||||
|         "about": "Easily edit and add OpenStreetMap for a certain theme", | ||||
|         "aboutMapcomplete": "<h3>About</h3><p>Use MapComplete to add OpenStreetMap info on a <b>single theme.</b> Answer questions, and within minutes your contributions are available everywhere. In most themes you can add pictures or even leave a review. The <b>theme maintainer</b> defines elements, questions and languages for it.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete.</li><li>The fullscreen version offers info about OpenStreetMap.</li><li>Viewing works without login, but editing requires an OSM account.</li><li>If you are not logged in, you are asked to do so</li><li>Once you answered a single question, you can add new features to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='{osmcha_link}' target='_blank' >OsmCha</a>.</p>", | ||||
|         "aboutMapcomplete": "<p>Use MapComplete to add OpenStreetMap info on a <b>single theme.</b> Answer questions, and within minutes your contributions are available everywhere. In most themes you can add pictures or even leave a review. The <b>theme maintainer</b> defines elements, questions and languages for it.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete.</li><li>The fullscreen version offers info about OpenStreetMap.</li><li>Viewing works without login, but editing requires an OSM account.</li><li>If you are not logged in, you are asked to do so</li><li>Once you answered a single question, you can add new features to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='{osmcha_link}' target='_blank' >OsmCha</a>.</p>", | ||||
|         "add": { | ||||
|             "addNew": "Add {category}", | ||||
|             "addNewMapLabel": "Click here to add a new item", | ||||
|  | @ -203,6 +203,10 @@ | |||
|         "loginToStart": "Log in to answer this question", | ||||
|         "loginWithOpenStreetMap": "Login with OpenStreetMap", | ||||
|         "logout": "Log out", | ||||
|         "menu": { | ||||
|             "aboutMapComplete": "About MapComplete", | ||||
|             "filter": "Filter data" | ||||
|         }, | ||||
|         "morescreen": { | ||||
|             "createYourOwnTheme": "Create your own MapComplete theme from scratch", | ||||
|             "hiddenExplanation": "These themes are only accessible to those with the link. You have discovered {hidden_discovered} of {total_hidden} hidden themes.", | ||||
|  |  | |||
|  | @ -1809,9 +1809,6 @@ | |||
|     "gps_track": { | ||||
|         "name": "La teva traça recorreguda" | ||||
|     }, | ||||
|     "grass_in_parks": { | ||||
|         "description": "Cerques per a tots els camins d'herba accessibles dins dels parcs públics - aquests són «groenzones»" | ||||
|     }, | ||||
|     "hackerspace": { | ||||
|         "presets": { | ||||
|             "1": { | ||||
|  |  | |||
|  | @ -4750,9 +4750,6 @@ | |||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "grass_in_parks": { | ||||
|         "description": "Sucht nach allen zugänglichen Grasflächen in öffentlichen Parks - dies sind 'Grünzonen'" | ||||
|     }, | ||||
|     "hackerspace": { | ||||
|         "description": "Hackerspace", | ||||
|         "name": "Hackerspaces", | ||||
|  |  | |||
|  | @ -4750,9 +4750,6 @@ | |||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "grass_in_parks": { | ||||
|         "description": "Searches for all accessible grass patches within public parks - these are 'groenzones'" | ||||
|     }, | ||||
|     "hackerspace": { | ||||
|         "description": "Hackerspace", | ||||
|         "name": "Hackerspace", | ||||
|  |  | |||
|  | @ -4657,18 +4657,6 @@ | |||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "grass_in_parks": { | ||||
|         "description": "Dit zoekt naar alle toegankelijke grasvelden binnen publieke parken - dit zijn 'groenzones'", | ||||
|         "name": "Toegankelijke grasvelden in parken", | ||||
|         "title": { | ||||
|             "mappings": { | ||||
|                 "0": { | ||||
|                     "then": "{name}" | ||||
|                 } | ||||
|             }, | ||||
|             "render": "Speelweide in een park" | ||||
|         } | ||||
|     }, | ||||
|     "hackerspace": { | ||||
|         "description": "Hackerspace", | ||||
|         "name": "Hackerspace", | ||||
|  |  | |||
|  | @ -661,7 +661,7 @@ | |||
|     "grb": { | ||||
|         "description": "Aquest tema és un intent d'automatitzar la importació GRB.", | ||||
|         "layers": { | ||||
|             "1": { | ||||
|             "0": { | ||||
|                 "tagRenderings": { | ||||
|                     "building type": { | ||||
|                         "question": "Quin tipus d'edifici és aquest?" | ||||
|  |  | |||
|  | @ -614,14 +614,14 @@ | |||
|     }, | ||||
|     "grb": { | ||||
|         "layers": { | ||||
|             "1": { | ||||
|             "0": { | ||||
|                 "tagRenderings": { | ||||
|                     "building type": { | ||||
|                         "question": "Jaký druh budovy je toto?" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "6": { | ||||
|             "5": { | ||||
|                 "tagRenderings": { | ||||
|                     "Import-button": { | ||||
|                         "mappings": { | ||||
|  |  | |||
|  | @ -552,14 +552,14 @@ | |||
|     "grb": { | ||||
|         "description": "Dette tema er et forsøg på at hjælpe med at automatisere GRB-importen.", | ||||
|         "layers": { | ||||
|             "1": { | ||||
|             "0": { | ||||
|                 "tagRenderings": { | ||||
|                     "building type": { | ||||
|                         "question": "Hvad er det for en bygning?" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "6": { | ||||
|             "5": { | ||||
|                 "tagRenderings": { | ||||
|                     "Import-button": { | ||||
|                         "mappings": { | ||||
|  |  | |||
|  | @ -689,14 +689,14 @@ | |||
|     "grb": { | ||||
|         "description": "Dieses Thema ist ein Versuch, die Automatisierung des GRB-Imports zu unterstützen.", | ||||
|         "layers": { | ||||
|             "1": { | ||||
|             "0": { | ||||
|                 "tagRenderings": { | ||||
|                     "building type": { | ||||
|                         "question": "Was ist das für ein Gebäude?" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "6": { | ||||
|             "5": { | ||||
|                 "tagRenderings": { | ||||
|                     "Import-button": { | ||||
|                         "mappings": { | ||||
|  |  | |||
|  | @ -689,14 +689,14 @@ | |||
|     "grb": { | ||||
|         "description": "This theme is an attempt to help automating the GRB import.", | ||||
|         "layers": { | ||||
|             "1": { | ||||
|             "0": { | ||||
|                 "tagRenderings": { | ||||
|                     "building type": { | ||||
|                         "question": "What kind of building is this?" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "6": { | ||||
|             "5": { | ||||
|                 "tagRenderings": { | ||||
|                     "Import-button": { | ||||
|                         "mappings": { | ||||
|  |  | |||
|  | @ -689,14 +689,14 @@ | |||
|     "grb": { | ||||
|         "description": "Este tema es un intento de automatizar la importación GRB.", | ||||
|         "layers": { | ||||
|             "1": { | ||||
|             "0": { | ||||
|                 "tagRenderings": { | ||||
|                     "building type": { | ||||
|                         "question": "¿Qué tipo de edificio es este?" | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             "6": { | ||||
|             "5": { | ||||
|                 "tagRenderings": { | ||||
|                     "Import-button": { | ||||
|                         "mappings": { | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue