forked from MapComplete/MapComplete
		
	SpecialVis: allow import flows to work with multiple target layers
This commit is contained in:
		
							parent
							
								
									7872f22151
								
							
						
					
					
						commit
						915cad2253
					
				
					 6 changed files with 349 additions and 329 deletions
				
			
		|  | @ -1,129 +1,135 @@ | |||
| <script lang="ts"> | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||
|   import LocationInput from "../InputElement/Helpers/LocationInput.svelte" | ||||
|   import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|   import { Tiles } from "../../Models/TileRange" | ||||
|   import { Map as MlMap } from "maplibre-gl" | ||||
|   import { BBox } from "../../Logic/BBox" | ||||
|   import type { MapProperties } from "../../Models/MapProperties" | ||||
|   import ShowDataLayer from "../Map/ShowDataLayer" | ||||
|   import type { | ||||
|     FeatureSource, | ||||
|     FeatureSourceForLayer, | ||||
|   } from "../../Logic/FeatureSource/FeatureSource" | ||||
|   import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource" | ||||
|   import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger" | ||||
|   import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|   import { Utils } from "../../Utils" | ||||
|   import { createEventDispatcher } from "svelte" | ||||
|   import Move_arrows from "../../assets/svg/Move_arrows.svelte" | ||||
|     import type { SpecialVisualizationState } from "../SpecialVisualization" | ||||
|     import LocationInput from "../InputElement/Helpers/LocationInput.svelte" | ||||
|     import { UIEventSource } from "../../Logic/UIEventSource" | ||||
|     import { Tiles } from "../../Models/TileRange" | ||||
|     import { Map as MlMap } from "maplibre-gl" | ||||
|     import { BBox } from "../../Logic/BBox" | ||||
|     import type { MapProperties } from "../../Models/MapProperties" | ||||
|     import ShowDataLayer from "../Map/ShowDataLayer" | ||||
|     import type { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource" | ||||
|     import SnappingFeatureSource from "../../Logic/FeatureSource/Sources/SnappingFeatureSource" | ||||
|     import FeatureSourceMerger from "../../Logic/FeatureSource/Sources/FeatureSourceMerger" | ||||
|     import LayerConfig from "../../Models/ThemeConfig/LayerConfig" | ||||
|     import { Utils } from "../../Utils" | ||||
|     import { createEventDispatcher } from "svelte" | ||||
|     import Move_arrows from "../../assets/svg/Move_arrows.svelte" | ||||
| 
 | ||||
|   /** | ||||
|    * An advanced location input, which has support to: | ||||
|    * - Show more layers | ||||
|    * - Snap to layers | ||||
|    * | ||||
|    * This one is mostly used to insert new points, including when importing | ||||
|    */ | ||||
|   export let state: SpecialVisualizationState | ||||
|   /** | ||||
|    * The start coordinate | ||||
|    */ | ||||
|   export let coordinate: { lon: number; lat: number } | ||||
|     /** | ||||
|      * An advanced location input, which has support to: | ||||
|      * - Show more layers | ||||
|      * - Snap to layers | ||||
|      * | ||||
|      * This one is mostly used to insert new points, including when importing | ||||
|      */ | ||||
|     export let state: SpecialVisualizationState | ||||
|     /** | ||||
|      * The start coordinate | ||||
|      */ | ||||
|     export let coordinate: { lon: number; lat: number } | ||||
| 
 | ||||
|   /** | ||||
|    * The center of the map at all times | ||||
|    * If undefined at the beginning, 'coordinate' will be used | ||||
|    */ | ||||
|   export let value: UIEventSource<{ lon: number; lat: number }> | ||||
|   if (value.data === undefined) { | ||||
|     value.setData(coordinate) | ||||
|   } | ||||
|   if (coordinate === undefined) { | ||||
|     coordinate = value.data | ||||
|   } | ||||
|   export let snapToLayers: string[] | undefined | ||||
|   export let targetLayer: LayerConfig | undefined | ||||
|   export let maxSnapDistance: number = undefined | ||||
| 
 | ||||
|   export let snappedTo: UIEventSource<string | undefined> | ||||
| 
 | ||||
|   let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{ | ||||
|     lon: number | ||||
|     lat: number | ||||
|   }>(undefined) | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>() | ||||
| 
 | ||||
|   const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16) | ||||
|   const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined) | ||||
|   let initialMapProperties: Partial<MapProperties> = { | ||||
|     zoom: new UIEventSource<number>(19), | ||||
|     maxbounds: new UIEventSource(undefined), | ||||
|     /*If no snapping needed: the value is simply the map location; | ||||
|      * If snapping is needed: the value will be set later on by the snapping feature source | ||||
|      * */ | ||||
|     location: | ||||
|       snapToLayers?.length > 0 | ||||
|         ? new UIEventSource<{ lon: number; lat: number }>(coordinate) | ||||
|         : value, | ||||
|     bounds: new UIEventSource<BBox>(undefined), | ||||
|     allowMoving: new UIEventSource<boolean>(true), | ||||
|     allowZooming: new UIEventSource<boolean>(true), | ||||
|     minzoom: new UIEventSource<number>(18), | ||||
|     rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer), | ||||
|   } | ||||
| 
 | ||||
|   if (targetLayer) { | ||||
|     const featuresForLayer = state.perLayer.get(targetLayer.id) | ||||
|     if (featuresForLayer) { | ||||
|       new ShowDataLayer(map, { | ||||
|         layer: targetLayer, | ||||
|         features: featuresForLayer, | ||||
|       }) | ||||
|     /** | ||||
|      * The center of the map at all times | ||||
|      * If undefined at the beginning, 'coordinate' will be used | ||||
|      */ | ||||
|     export let value: UIEventSource<{ lon: number; lat: number }> | ||||
|     if (value.data === undefined) { | ||||
|         value.setData(coordinate) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (snapToLayers?.length > 0) { | ||||
|     const snapSources: FeatureSource[] = [] | ||||
|     for (const layerId of snapToLayers ?? []) { | ||||
|       const layer: FeatureSourceForLayer = state.perLayer.get(layerId) | ||||
|       snapSources.push(layer) | ||||
|       if (layer.features === undefined) { | ||||
|         continue | ||||
|       } | ||||
|       new ShowDataLayer(map, { | ||||
|         layer: layer.layer.layerDef, | ||||
|         zoomToFeatures: false, | ||||
|         features: layer, | ||||
|       }) | ||||
|     if (coordinate === undefined) { | ||||
|         coordinate = value.data | ||||
|     } | ||||
|     const snappedLocation = new SnappingFeatureSource( | ||||
|       new FeatureSourceMerger(...Utils.NoNull(snapSources)), | ||||
|       // We snap to the (constantly updating) map location | ||||
|       initialMapProperties.location, | ||||
|       { | ||||
|         maxDistance: maxSnapDistance ?? 15, | ||||
|         allowUnsnapped: true, | ||||
|         snappedTo, | ||||
|         snapLocation: value, | ||||
|       } | ||||
|     ) | ||||
|     export let snapToLayers: string[] | undefined | ||||
|     export let targetLayer: LayerConfig | LayerConfig[] | undefined | ||||
| 
 | ||||
|     new ShowDataLayer(map, { | ||||
|       layer: targetLayer, | ||||
|       features: snappedLocation, | ||||
|     let targetLayers: LayerConfig[] | undefined | ||||
|     if (Array.isArray(targetLayers)) { | ||||
|         targetLayers = <LayerConfig[]>targetLayer | ||||
|     } else if (targetLayer) { | ||||
|         targetLayers = [<LayerConfig>targetLayer] | ||||
|     } | ||||
|     export let maxSnapDistance: number = undefined | ||||
| 
 | ||||
|     export let snappedTo: UIEventSource<string | undefined> | ||||
| 
 | ||||
|     let preciseLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource<{ | ||||
|         lon: number | ||||
|         lat: number | ||||
|     }>(undefined) | ||||
| 
 | ||||
|     const dispatch = createEventDispatcher<{ click: { lon: number; lat: number } }>() | ||||
| 
 | ||||
|     const xyz = Tiles.embedded_tile(coordinate.lat, coordinate.lon, 16) | ||||
|     const map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined) | ||||
|     let initialMapProperties: Partial<MapProperties> = { | ||||
|         zoom: new UIEventSource<number>(19), | ||||
|         maxbounds: new UIEventSource(undefined), | ||||
|         /*If no snapping needed: the value is simply the map location; | ||||
|          * If snapping is needed: the value will be set later on by the snapping feature source | ||||
|          * */ | ||||
|         location: | ||||
|             snapToLayers?.length > 0 | ||||
|                 ? new UIEventSource<{ lon: number; lat: number }>(coordinate) | ||||
|                 : value, | ||||
|         bounds: new UIEventSource<BBox>(undefined), | ||||
|         allowMoving: new UIEventSource<boolean>(true), | ||||
|         allowZooming: new UIEventSource<boolean>(true), | ||||
|         minzoom: new UIEventSource<number>(18), | ||||
|         rasterLayer: UIEventSource.feedFrom(state.mapProperties.rasterLayer), | ||||
|     } | ||||
| 
 | ||||
|     targetLayers?.forEach(layer => { | ||||
|         const featuresForLayer = state.perLayer.get(layer.id) | ||||
|         if (featuresForLayer) { | ||||
|             new ShowDataLayer(map, { | ||||
|                 layer, | ||||
|                 features: featuresForLayer, | ||||
|             }) | ||||
|         } | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|     if (snapToLayers?.length > 0) { | ||||
|         const snapSources: FeatureSource[] = [] | ||||
|         for (const layerId of snapToLayers ?? []) { | ||||
|             const layer: FeatureSourceForLayer = state.perLayer.get(layerId) | ||||
|             snapSources.push(layer) | ||||
|             if (layer.features === undefined) { | ||||
|                 continue | ||||
|             } | ||||
|             new ShowDataLayer(map, { | ||||
|                 layer: layer.layer.layerDef, | ||||
|                 zoomToFeatures: false, | ||||
|                 features: layer, | ||||
|             }) | ||||
|         } | ||||
|         const snappedLocation = new SnappingFeatureSource( | ||||
|             new FeatureSourceMerger(...Utils.NoNull(snapSources)), | ||||
|             // We snap to the (constantly updating) map location | ||||
|             initialMapProperties.location, | ||||
|             { | ||||
|                 maxDistance: maxSnapDistance ?? 15, | ||||
|                 allowUnsnapped: true, | ||||
|                 snappedTo, | ||||
|                 snapLocation: value, | ||||
|             }, | ||||
|         ) | ||||
| 
 | ||||
|         targetLayers.forEach(layer => { | ||||
|             new ShowDataLayer(map, { | ||||
|                 layer, | ||||
|                 features: snappedLocation, | ||||
|             }) | ||||
|         }) | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <LocationInput | ||||
|   {map} | ||||
|   on:click={(data) => dispatch("click", data)} | ||||
|   mapProperties={initialMapProperties} | ||||
|   value={preciseLocation} | ||||
|   initialCoordinate={coordinate} | ||||
|   maxDistanceInMeters="50" | ||||
|   {map} | ||||
|   mapProperties={initialMapProperties} | ||||
|   maxDistanceInMeters={50} | ||||
|   on:click={(data) => dispatch("click", data)} | ||||
|   value={preciseLocation} | ||||
| > | ||||
|   <slot name="image" slot="image"> | ||||
|     <Move_arrows class="h-full max-h-24" /> | ||||
|  |  | |||
|  | @ -1,41 +1,57 @@ | |||
| <script lang="ts"> | ||||
|   /** | ||||
|    * The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ... | ||||
|    * They show some default components | ||||
|    */ | ||||
|   import ImportFlow from "./ImportFlow" | ||||
|   import LoginToggle from "../../Base/LoginToggle.svelte" | ||||
|   import BackButton from "../../Base/BackButton.svelte" | ||||
|   import Translations from "../../i18n/Translations" | ||||
|   import Tr from "../../Base/Tr.svelte" | ||||
|   import NextButton from "../../Base/NextButton.svelte" | ||||
|   import { createEventDispatcher } from "svelte" | ||||
|   import Loading from "../../Base/Loading.svelte" | ||||
|   import { And } from "../../../Logic/Tags/And" | ||||
|   import TagHint from "../TagHint.svelte" | ||||
|   import { TagsFilter } from "../../../Logic/Tags/TagsFilter" | ||||
|   import { Store } from "../../../Logic/UIEventSource" | ||||
|   import Svg from "../../../Svg" | ||||
|   import ToSvelte from "../../Base/ToSvelte.svelte" | ||||
|   import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|     import type { ImportFlowArguments } from "./ImportFlow" | ||||
|     /** | ||||
|      * The 'importflow' does some basic setup, e.g. validate that imports are allowed, that the user is logged-in, ... | ||||
|      * They show some default components | ||||
|      */ | ||||
|     import ImportFlow from "./ImportFlow" | ||||
|     import LoginToggle from "../../Base/LoginToggle.svelte" | ||||
|     import BackButton from "../../Base/BackButton.svelte" | ||||
|     import Translations from "../../i18n/Translations" | ||||
|     import Tr from "../../Base/Tr.svelte" | ||||
|     import NextButton from "../../Base/NextButton.svelte" | ||||
|     import { createEventDispatcher, onDestroy } from "svelte" | ||||
|     import Loading from "../../Base/Loading.svelte" | ||||
|     import { And } from "../../../Logic/Tags/And" | ||||
|     import TagHint from "../TagHint.svelte" | ||||
|     import { TagsFilter } from "../../../Logic/Tags/TagsFilter" | ||||
|     import { Store } from "../../../Logic/UIEventSource" | ||||
|     import Svg from "../../../Svg" | ||||
|     import ToSvelte from "../../Base/ToSvelte.svelte" | ||||
|     import { EyeIcon, EyeOffIcon } from "@rgossiaux/svelte-heroicons/solid" | ||||
|     import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| 
 | ||||
|   export let importFlow: ImportFlow | ||||
|   let state = importFlow.state | ||||
|     export let importFlow: ImportFlow<ImportFlowArguments> | ||||
|     let state = importFlow.state | ||||
| 
 | ||||
|   export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start" | ||||
|     export let currentFlowStep: "start" | "confirm" | "importing" | "imported" = "start" | ||||
| 
 | ||||
|   const isLoading = state.dataIsLoading | ||||
|   const dispatch = createEventDispatcher<{ confirm }>() | ||||
|   const canBeImported = importFlow.canBeImported() | ||||
|   const tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags)) | ||||
|     const isLoading = state.dataIsLoading | ||||
|     let dispatch = createEventDispatcher<{ confirm }>() | ||||
|     let canBeImported = importFlow.canBeImported() | ||||
|     let tags: Store<TagsFilter> = importFlow.tagsToApply.map((tags) => new And(tags)) | ||||
| 
 | ||||
|   const isDisplayed = importFlow.targetLayer.isDisplayed | ||||
|   const hasFilter = importFlow.targetLayer.hasFilter | ||||
| 
 | ||||
|   function abort() { | ||||
|     state.selectedElement.setData(undefined) | ||||
|     state.selectedLayer.setData(undefined) | ||||
|   } | ||||
|     let targetLayers = importFlow.targetLayer | ||||
|     let filteredLayer: FilteredLayer | ||||
|     let undisplayedLayer: FilteredLayer | ||||
| 
 | ||||
|     function updateIsDisplayed() { | ||||
|         filteredLayer = targetLayers.find(tl => tl.hasFilter.data) | ||||
|         undisplayedLayer = targetLayers.find(tl => !tl.isDisplayed.data) | ||||
|     } | ||||
| 
 | ||||
|     updateIsDisplayed() | ||||
|      | ||||
|     for (const tl of targetLayers) { | ||||
|         onDestroy( | ||||
|             tl.isDisplayed.addCallback(updateIsDisplayed), | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     function abort() { | ||||
|         state.selectedElement.setData(undefined) | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <LoginToggle {state}> | ||||
|  | @ -44,13 +60,13 @@ | |||
|     {#if $canBeImported.extraHelp} | ||||
|       <Tr t={$canBeImported.extraHelp} /> | ||||
|     {/if} | ||||
|   {:else if !$isDisplayed} | ||||
|   {:else if undisplayedLayer !== undefined} | ||||
|     <!-- Check that the layer is enabled, so that we don't add a duplicate --> | ||||
|     <div class="alert flex items-center justify-center"> | ||||
|       <EyeOffIcon class="w-8" /> | ||||
|       <Tr | ||||
|         t={Translations.t.general.add.layerNotEnabled.Subs({ | ||||
|           layer: importFlow.targetLayer.layerDef.name, | ||||
|           layer: undisplayedLayer.layerDef.name, | ||||
|         })} | ||||
|       /> | ||||
|     </div> | ||||
|  | @ -60,7 +76,7 @@ | |||
|         class="flex w-full gap-x-1" | ||||
|         on:click={() => { | ||||
|           abort() | ||||
|           state.guistate.openFilterView(importFlow.targetLayer.layerDef) | ||||
|           state.guistate.openFilterView(filteredLayer.layerDef) | ||||
|         }} | ||||
|       > | ||||
|         <ToSvelte construct={Svg.layers_svg().SetClass("w-12")} /> | ||||
|  | @ -70,19 +86,19 @@ | |||
|       <button | ||||
|         class="primary flex w-full gap-x-1" | ||||
|         on:click={() => { | ||||
|           isDisplayed.setData(true) | ||||
|           undisplayedLayer.isDisplayed.setData(true) | ||||
|           abort() | ||||
|         }} | ||||
|       > | ||||
|         <EyeIcon class="w-12" /> | ||||
|         <Tr | ||||
|           t={Translations.t.general.add.enableLayer.Subs({ | ||||
|             name: importFlow.targetLayer.layerDef.name, | ||||
|             name: undisplayedLayer.layerDef.name, | ||||
|           })} | ||||
|         /> | ||||
|       </button> | ||||
|     </div> | ||||
|   {:else if $hasFilter} | ||||
|   {:else if filteredLayer !== undefined} | ||||
|     <!-- Some filters are enabled. The feature to add might already be mapped, but hidden --> | ||||
|     <div class="alert flex items-center justify-center"> | ||||
|       <EyeOffIcon class="w-8" /> | ||||
|  | @ -93,7 +109,7 @@ | |||
|         class="primary flex w-full gap-x-1" | ||||
|         on:click={() => { | ||||
|           abort() | ||||
|           importFlow.targetLayer.disableAllFilters() | ||||
|           filteredLayer.disableAllFilters() | ||||
|         }} | ||||
|       > | ||||
|         <EyeOffIcon class="w-12" /> | ||||
|  | @ -103,7 +119,7 @@ | |||
|         class="flex w-full gap-x-1" | ||||
|         on:click={() => { | ||||
|           abort() | ||||
|           state.guistate.openFilterView(importFlow.targetLayer.layerDef) | ||||
|           state.guistate.openFilterView(filteredLayer.layerDef) | ||||
|         }} | ||||
|       > | ||||
|         <ToSvelte construct={Svg.layers_svg().SetClass("w-12")} /> | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ import TagApplyButton from "../TagApplyButton" | |||
| import { PointImportFlowArguments } from "./PointImportFlowState" | ||||
| import { Translation } from "../../i18n/Translation" | ||||
| import Translations from "../../i18n/Translations" | ||||
| import { OsmConnection } from "../../../Logic/Osm/OsmConnection" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||
| import { LayerConfigJson } from "../../../Models/ThemeConfig/Json/LayerConfigJson" | ||||
|  | @ -25,7 +24,7 @@ export class ImportFlowUtils { | |||
|     public static readonly conflationLayer = new LayerConfig( | ||||
|         <LayerConfigJson>conflation_json, | ||||
|         "all_known_layers", | ||||
|         true | ||||
|         true, | ||||
|     ) | ||||
| 
 | ||||
|     public static readonly documentationGeneral = `\n\n\nNote that the contributor must zoom to at least zoomlevel 18 to be able to use this functionality.
 | ||||
|  | @ -67,7 +66,7 @@ ${Utils.special_visualizations_importRequirementDocs} | |||
|      */ | ||||
|     public static getTagsToApply( | ||||
|         originalFeatureTags: UIEventSource<any>, | ||||
|         args: { tags: string } | ||||
|         args: { tags: string }, | ||||
|     ): Store<Tag[]> { | ||||
|         if (originalFeatureTags === undefined) { | ||||
|             return undefined | ||||
|  | @ -83,9 +82,9 @@ ${Utils.special_visualizations_importRequirementDocs} | |||
|             const items: string = originalFeatureTags.data[tags] | ||||
|             console.debug( | ||||
|                 "The import button is using tags from properties[" + | ||||
|                     tags + | ||||
|                     "] of this object, namely ", | ||||
|                 items | ||||
|                 tags + | ||||
|                 "] of this object, namely ", | ||||
|                 items, | ||||
|             ) | ||||
| 
 | ||||
|             if (items.startsWith("{")) { | ||||
|  | @ -108,13 +107,12 @@ ${Utils.special_visualizations_importRequirementDocs} | |||
|      * - targetLayer | ||||
|      * | ||||
|      * Others (e.g.: snapOnto-layers) are not to be handled here | ||||
|      * @param argsRaw | ||||
|      */ | ||||
|     public static getLayerDependencies(argsRaw: string[], argSpec?) { | ||||
|     public static getLayerDependencies(argsRaw: string[], argSpec?): string[] { | ||||
|         const args: ImportFlowArguments = <any>( | ||||
|             Utils.ParseVisArgs(argSpec ?? ImportFlowUtils.generalArguments, argsRaw) | ||||
|         ) | ||||
|         return [args.targetLayer] | ||||
|         return args.targetLayer.split(" ") | ||||
|     } | ||||
| 
 | ||||
|     public static getLayerDependenciesWithSnapOnto( | ||||
|  | @ -122,7 +120,7 @@ ${Utils.special_visualizations_importRequirementDocs} | |||
|             name: string | ||||
|             defaultValue?: string | ||||
|         }[], | ||||
|         argsRaw: string[] | ||||
|         argsRaw: string[], | ||||
|     ): string[] { | ||||
|         const deps = ImportFlowUtils.getLayerDependencies(argsRaw, argSpec) | ||||
|         const argsParsed: PointImportFlowArguments = <any>Utils.ParseVisArgs(argSpec, argsRaw) | ||||
|  | @ -130,30 +128,6 @@ ${Utils.special_visualizations_importRequirementDocs} | |||
|         deps.push(...snapOntoLayers) | ||||
|         return deps | ||||
|     } | ||||
| 
 | ||||
|     public static buildTagSpec( | ||||
|         args: ImportFlowArguments, | ||||
|         tagSource: Store<Record<string, string>> | ||||
|     ): Store<string> { | ||||
|         let tagSpec = args.tags | ||||
|         return tagSource.mapD((tags) => { | ||||
|             if ( | ||||
|                 tagSpec.indexOf(" ") < 0 && | ||||
|                 tagSpec.indexOf(";") < 0 && | ||||
|                 tags[args.tags] !== undefined | ||||
|             ) { | ||||
|                 // This is probably a key
 | ||||
|                 tagSpec = tags[args.tags] | ||||
|                 console.debug( | ||||
|                     "The import button is using tags from properties[" + | ||||
|                         args.tags + | ||||
|                         "] of this object, namely ", | ||||
|                     tagSpec | ||||
|                 ) | ||||
|             } | ||||
|             return tagSpec | ||||
|         }) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  | @ -164,7 +138,7 @@ ${Utils.special_visualizations_importRequirementDocs} | |||
| export default abstract class ImportFlow<ArgT extends ImportFlowArguments> { | ||||
|     public readonly state: SpecialVisualizationState | ||||
|     public readonly args: ArgT | ||||
|     public readonly targetLayer: FilteredLayer | ||||
|     public readonly targetLayer: FilteredLayer[] | ||||
|     public readonly tagsToApply: Store<Tag[]> | ||||
|     protected readonly _originalFeatureTags: UIEventSource<Record<string, string>> | ||||
| 
 | ||||
|  | @ -172,13 +146,19 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> { | |||
|         state: SpecialVisualizationState, | ||||
|         args: ArgT, | ||||
|         tagsToApply: Store<Tag[]>, | ||||
|         originalTags: UIEventSource<Record<string, string>> | ||||
|         originalTags: UIEventSource<Record<string, string>>, | ||||
|     ) { | ||||
|         this.state = state | ||||
|         this.args = args | ||||
|         this.tagsToApply = tagsToApply | ||||
|         this._originalFeatureTags = originalTags | ||||
|         this.targetLayer = state.layerState.filteredLayers.get(args.targetLayer) | ||||
|         this.targetLayer = args.targetLayer.split(" ").map(tl => { | ||||
|             let found = state.layerState.filteredLayers.get(tl) | ||||
|             if (!found) { | ||||
|                 throw "Layer " + tl + " not found" | ||||
|             } | ||||
|             return found | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -218,7 +198,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> { | |||
| 
 | ||||
|                 return undefined | ||||
|             }, | ||||
|             [state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags] | ||||
|             [state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags], | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ export class PointImportButtonViz implements SpecialVisualization { | |||
|     public readonly funcName: string | ||||
|     public readonly docs: string | BaseUIElement | ||||
|     public readonly example?: string | ||||
|     public readonly args: { name: string; defaultValue?: string; doc: string }[] | ||||
|     public readonly args: { name: string; defaultValue?: string; doc: string, split?: boolean }[] | ||||
|     public needsUrls = [] | ||||
| 
 | ||||
|     constructor() { | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ | |||
|   const args = importFlow.args | ||||
| 
 | ||||
|   // The following variables are used for the map | ||||
|   const targetLayer: LayerConfig = state.layout.layers.find((l) => l.id === args.targetLayer) | ||||
|   const targetLayers: LayerConfig[] = args.targetLayer.split(" ").map(tl => state.layout.layers.find((l) => l.id === tl)) | ||||
|   const snapToLayers: string[] | undefined = | ||||
|     args.snap_onto_layers?.split(",")?.map((l) => l.trim()) ?? [] | ||||
|   const maxSnapDistance: number = Number(args.max_snap_distance ?? 25) ?? 25 | ||||
|  | @ -33,21 +33,20 @@ | |||
| 
 | ||||
|   async function onConfirm(): Promise<void> { | ||||
|     const importedId = await importFlow.onConfirm(value.data, snappedTo.data) | ||||
|     state.selectedLayer.setData(targetLayer) | ||||
|     state.selectedElement.setData(state.indexedFeatures.featuresById.data.get(importedId)) | ||||
|   } | ||||
| </script> | ||||
| 
 | ||||
| <ImportFlow {importFlow} on:confirm={onConfirm}> | ||||
|   <div class="relative" slot="map"> | ||||
|     <div class="h-32"> | ||||
|     <div class="h-64"> | ||||
|       <NewPointLocationInput | ||||
|         coordinate={startCoordinate} | ||||
|         {maxSnapDistance} | ||||
|         {snapToLayers} | ||||
|         {snappedTo} | ||||
|         {state} | ||||
|         {targetLayer} | ||||
|         targetLayer={targetLayers} | ||||
|         {value} | ||||
|       /> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -3,11 +3,7 @@ import { FixedUiElement } from "./Base/FixedUiElement" | |||
| import BaseUIElement from "./BaseUIElement" | ||||
| import Title from "./Base/Title" | ||||
| import Table from "./Base/Table" | ||||
| import { | ||||
|     RenderingSpecification, | ||||
|     SpecialVisualization, | ||||
|     SpecialVisualizationState, | ||||
| } from "./SpecialVisualization" | ||||
| import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization" | ||||
| import { HistogramViz } from "./Popup/HistogramViz" | ||||
| import { MinimapViz } from "./Popup/MinimapViz" | ||||
| import { ShareLinkViz } from "./Popup/ShareLinkViz" | ||||
|  | @ -110,7 +106,7 @@ class NearbyImageVis implements SpecialVisualization { | |||
|         tags: UIEventSource<Record<string, string>>, | ||||
|         args: string[], | ||||
|         feature: Feature, | ||||
|         layer: LayerConfig | ||||
|         layer: LayerConfig, | ||||
|     ): BaseUIElement { | ||||
|         const isOpen = args[0] === "open" | ||||
|         const [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||
|  | @ -175,7 +171,7 @@ class StealViz implements SpecialVisualization { | |||
|                                 selectedElement: otherFeature, | ||||
|                                 state, | ||||
|                                 layer, | ||||
|                             }) | ||||
|                             }), | ||||
|                         ) | ||||
|                     } | ||||
|                     if (elements.length === 1) { | ||||
|  | @ -183,8 +179,8 @@ class StealViz implements SpecialVisualization { | |||
|                     } | ||||
|                     return new Combine(elements).SetClass("flex flex-col") | ||||
|                 }, | ||||
|                 [state.indexedFeatures.featuresById] | ||||
|             ) | ||||
|                 [state.indexedFeatures.featuresById], | ||||
|             ), | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|  | @ -223,7 +219,7 @@ export class QuestionViz implements SpecialVisualization { | |||
|         tags: UIEventSource<Record<string, string>>, | ||||
|         args: string[], | ||||
|         feature: Feature, | ||||
|         layer: LayerConfig | ||||
|         layer: LayerConfig, | ||||
|     ): BaseUIElement { | ||||
|         const labels = args[0] | ||||
|             ?.split(";") | ||||
|  | @ -277,17 +273,17 @@ export default class SpecialVisualizations { | |||
|      * templ.args[0] = "{email}" | ||||
|      */ | ||||
|     public static constructSpecification( | ||||
|         template: string, | ||||
|         extraMappings: SpecialVisualization[] = [] | ||||
|         template: string | { special: Record<string, string | Record<string, string>> & { type: string } }, | ||||
|         extraMappings: SpecialVisualization[] = [], | ||||
|     ): RenderingSpecification[] { | ||||
|         if (template === "") { | ||||
|             return [] | ||||
|         } | ||||
| 
 | ||||
|         if (template["type"] !== undefined) { | ||||
|         if (typeof template !== "string") { | ||||
|             console.trace( | ||||
|                 "Got a non-expanded template while constructing the specification, it still has a 'special-key':", | ||||
|                 template | ||||
|                 template, | ||||
|             ) | ||||
|             throw "Got a non-expanded template while constructing the specification" | ||||
|         } | ||||
|  | @ -295,20 +291,20 @@ export default class 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") | ||||
|                 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 | ||||
|                     extraMappings, | ||||
|                 ) | ||||
|                 const argument = | ||||
|                     matched[2] /* .trim()  // We don't trim, as spaces might be relevant, e.g. "what is ... of {title()}"*/ | ||||
|                 const style = matched[3]?.substring(1) ?? "" | ||||
|                 const partAfter = SpecialVisualizations.constructSpecification( | ||||
|                     matched[4], | ||||
|                     extraMappings | ||||
|                     extraMappings, | ||||
|                 ) | ||||
|                 const args = knownSpecial.args.map((arg) => arg.defaultValue ?? "") | ||||
|                 if (argument.length > 0) { | ||||
|  | @ -347,31 +343,31 @@ export default class SpecialVisualizations { | |||
|             viz.docs, | ||||
|             viz.args.length > 0 | ||||
|                 ? new Table( | ||||
|                       ["name", "default", "description"], | ||||
|                       viz.args.map((arg) => { | ||||
|                           let defaultArg = arg.defaultValue ?? "_undefined_" | ||||
|                           if (defaultArg == "") { | ||||
|                               defaultArg = "_empty string_" | ||||
|                           } | ||||
|                           return [arg.name, defaultArg, arg.doc] | ||||
|                       }) | ||||
|                   ) | ||||
|                     ["name", "default", "description"], | ||||
|                     viz.args.map((arg) => { | ||||
|                         let defaultArg = arg.defaultValue ?? "_undefined_" | ||||
|                         if (defaultArg == "") { | ||||
|                             defaultArg = "_empty string_" | ||||
|                         } | ||||
|                         return [arg.name, defaultArg, arg.doc] | ||||
|                     }), | ||||
|                 ) | ||||
|                 : undefined, | ||||
|             new Title("Example usage of " + viz.funcName, 4), | ||||
|             new FixedUiElement( | ||||
|                 viz.example ?? | ||||
|                     "`{" + | ||||
|                         viz.funcName + | ||||
|                         "(" + | ||||
|                         viz.args.map((arg) => arg.defaultValue).join(",") + | ||||
|                         ")}`" | ||||
|                 "`{" + | ||||
|                 viz.funcName + | ||||
|                 "(" + | ||||
|                 viz.args.map((arg) => arg.defaultValue).join(",") + | ||||
|                 ")}`", | ||||
|             ).SetClass("literal-code"), | ||||
|         ]) | ||||
|     } | ||||
| 
 | ||||
|     public static HelpMessage() { | ||||
|         const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) => | ||||
|             SpecialVisualizations.DocumentationFor(viz) | ||||
|             SpecialVisualizations.DocumentationFor(viz), | ||||
|         ) | ||||
| 
 | ||||
|         return new Combine([ | ||||
|  | @ -405,10 +401,10 @@ export default class SpecialVisualizations { | |||
|                             }, | ||||
|                         }, | ||||
|                         null, | ||||
|                         "  " | ||||
|                     ) | ||||
|                         "  ", | ||||
|                     ), | ||||
|                 ).SetClass("code"), | ||||
|                 'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)', | ||||
|                 "In other words: use `{ \"before\": ..., \"after\": ..., \"special\": {\"type\": ..., \"argname\": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)", | ||||
|             ]).SetClass("flex flex-col"), | ||||
|             ...helpTexts, | ||||
|         ]).SetClass("flex flex-col") | ||||
|  | @ -417,20 +413,20 @@ export default class SpecialVisualizations { | |||
|     // noinspection JSUnusedGlobalSymbols
 | ||||
|     public static renderExampleOfSpecial( | ||||
|         state: SpecialVisualizationState, | ||||
|         s: SpecialVisualization | ||||
|         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, | ||||
|                           undefined | ||||
|                       ) | ||||
|                   }) | ||||
|                     return s.constr( | ||||
|                         state, | ||||
|                         new UIEventSource<Record<string, string>>(e.feature.properties), | ||||
|                         e.args, | ||||
|                         e.feature, | ||||
|                         undefined, | ||||
|                     ) | ||||
|                 }) | ||||
|         return new Combine([new Title(s.funcName), s.docs, ...examples]) | ||||
|     } | ||||
| 
 | ||||
|  | @ -470,7 +466,7 @@ export default class SpecialVisualizations { | |||
|                         assignTo: state.userRelatedState.language, | ||||
|                         availableLanguages: state.layout.language, | ||||
|                         preferredLanguages: state.osmConnection.userDetails.map( | ||||
|                             (ud) => ud.languages | ||||
|                             (ud) => ud.languages, | ||||
|                         ), | ||||
|                     }) | ||||
|                 }, | ||||
|  | @ -495,7 +491,7 @@ export default class SpecialVisualizations { | |||
| 
 | ||||
|                 constr( | ||||
|                     state: SpecialVisualizationState, | ||||
|                     tagSource: UIEventSource<Record<string, string>> | ||||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                 ): BaseUIElement { | ||||
|                     return new VariableUiElement( | ||||
|                         tagSource | ||||
|  | @ -505,7 +501,7 @@ export default class SpecialVisualizations { | |||
|                                     return new SplitRoadWizard(<WayId>id, state) | ||||
|                                 } | ||||
|                                 return undefined | ||||
|                             }) | ||||
|                             }), | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|  | @ -519,7 +515,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     if (feature.geometry.type !== "Point") { | ||||
|                         return undefined | ||||
|  | @ -542,7 +538,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     if (!layer.deletion) { | ||||
|                         return undefined | ||||
|  | @ -570,7 +566,7 @@ export default class SpecialVisualizations { | |||
|                     state: SpecialVisualizationState, | ||||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature | ||||
|                     feature: Feature, | ||||
|                 ): BaseUIElement { | ||||
|                     const [lon, lat] = GeoOperations.centerpointCoordinates(feature) | ||||
|                     return new SvelteUIElement(CreateNewNote, { | ||||
|  | @ -634,7 +630,7 @@ export default class SpecialVisualizations { | |||
|                             .map((tags) => tags[args[0]]) | ||||
|                             .map((wikidata) => { | ||||
|                                 wikidata = Utils.NoEmpty( | ||||
|                                     wikidata?.split(";")?.map((wd) => wd.trim()) ?? [] | ||||
|                                     wikidata?.split(";")?.map((wd) => wd.trim()) ?? [], | ||||
|                                 )[0] | ||||
|                                 const entry = Wikidata.LoadWikidataEntry(wikidata) | ||||
|                                 return new VariableUiElement( | ||||
|  | @ -644,9 +640,9 @@ export default class SpecialVisualizations { | |||
|                                         } | ||||
|                                         const response = <WikidataResponse>e["success"] | ||||
|                                         return Translation.fromMap(response.labels) | ||||
|                                     }) | ||||
|                                     }), | ||||
|                                 ) | ||||
|                             }) | ||||
|                             }), | ||||
|                     ), | ||||
|             }, | ||||
|             new MapillaryLinkVis(), | ||||
|  | @ -678,7 +674,7 @@ export default class SpecialVisualizations { | |||
|                         AllImageProviders.LoadImagesFor(tags, imagePrefixes), | ||||
|                         tags, | ||||
|                         state, | ||||
|                         feature | ||||
|                         feature, | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|  | @ -734,7 +730,7 @@ export default class SpecialVisualizations { | |||
|                         { | ||||
|                             nameKey: nameKey, | ||||
|                             fallbackName, | ||||
|                         } | ||||
|                         }, | ||||
|                     ) | ||||
|                     return new SvelteUIElement(StarsBarIcon, { | ||||
|                         score: reviews.average, | ||||
|  | @ -767,7 +763,7 @@ export default class SpecialVisualizations { | |||
|                         { | ||||
|                             nameKey: nameKey, | ||||
|                             fallbackName, | ||||
|                         } | ||||
|                         }, | ||||
|                     ) | ||||
|                     return new SvelteUIElement(ReviewForm, { reviews, state, tags, feature, layer }) | ||||
|                 }, | ||||
|  | @ -799,7 +795,7 @@ export default class SpecialVisualizations { | |||
|                         { | ||||
|                             nameKey: nameKey, | ||||
|                             fallbackName, | ||||
|                         } | ||||
|                         }, | ||||
|                     ) | ||||
|                     return new SvelteUIElement(AllReviews, { reviews, state, tags, feature, layer }) | ||||
|                 }, | ||||
|  | @ -857,7 +853,7 @@ export default class SpecialVisualizations { | |||
|                     tags: UIEventSource<Record<string, string>>, | ||||
|                     args: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): SvelteUIElement { | ||||
|                     const keyToUse = args[0] | ||||
|                     const prefix = args[1] | ||||
|  | @ -894,10 +890,10 @@ export default class SpecialVisualizations { | |||
|                                     return undefined | ||||
|                                 } | ||||
|                                 const allUnits: Unit[] = [].concat( | ||||
|                                     ...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []) | ||||
|                                     ...(state?.layout?.layers?.map((lyr) => lyr.units) ?? []), | ||||
|                                 ) | ||||
|                                 const unit = allUnits.filter((unit) => | ||||
|                                     unit.isApplicableToKey(key) | ||||
|                                     unit.isApplicableToKey(key), | ||||
|                                 )[0] | ||||
|                                 if (unit === undefined) { | ||||
|                                     return value | ||||
|  | @ -905,7 +901,7 @@ export default class SpecialVisualizations { | |||
|                                 const getCountry = () => tagSource.data._country | ||||
|                                 const [v, denom] = unit.findDenomination(value, getCountry) | ||||
|                                 return unit.asHumanLongValue(v, getCountry) | ||||
|                             }) | ||||
|                             }), | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|  | @ -922,7 +918,7 @@ export default class SpecialVisualizations { | |||
|                         new Combine([ | ||||
|                             t.downloadFeatureAsGeojson.SetClass("font-bold text-lg"), | ||||
|                             t.downloadGeoJsonHelper.SetClass("subtle"), | ||||
|                         ]).SetClass("flex flex-col") | ||||
|                         ]).SetClass("flex flex-col"), | ||||
|                     ) | ||||
|                         .onClick(() => { | ||||
|                             console.log("Exporting as Geojson") | ||||
|  | @ -935,7 +931,7 @@ export default class SpecialVisualizations { | |||
|                                 title + "_mapcomplete_export.geojson", | ||||
|                                 { | ||||
|                                     mimetype: "application/vnd.geo+json", | ||||
|                                 } | ||||
|                                 }, | ||||
|                             ) | ||||
|                         }) | ||||
|                         .SetClass("w-full") | ||||
|  | @ -971,7 +967,7 @@ export default class SpecialVisualizations { | |||
|                 constr: (state) => { | ||||
|                     return new SubtleButton( | ||||
|                         Svg.delete_icon_svg().SetStyle("height: 1.5rem"), | ||||
|                         Translations.t.general.removeLocationHistory | ||||
|                         Translations.t.general.removeLocationHistory, | ||||
|                     ).onClick(() => { | ||||
|                         state.historicalUserLocations.features.setData([]) | ||||
|                         state.selectedElement.setData(undefined) | ||||
|  | @ -1009,10 +1005,10 @@ export default class SpecialVisualizations { | |||
|                                         .filter((c) => c.text !== "") | ||||
|                                         .map( | ||||
|                                             (c, i) => | ||||
|                                                 new NoteCommentElement(c, state, i, comments.length) | ||||
|                                         ) | ||||
|                                                 new NoteCommentElement(c, state, i, comments.length), | ||||
|                                         ), | ||||
|                                 ).SetClass("flex flex-col") | ||||
|                             }) | ||||
|                             }), | ||||
|                     ), | ||||
|             }, | ||||
|             { | ||||
|  | @ -1053,9 +1049,9 @@ export default class SpecialVisualizations { | |||
|                                 return undefined | ||||
|                             } | ||||
|                             return new SubstitutedTranslation(title, tagsSource, state).SetClass( | ||||
|                                 "px-1" | ||||
|                                 "px-1", | ||||
|                             ) | ||||
|                         }) | ||||
|                         }), | ||||
|                     ), | ||||
|             }, | ||||
|             { | ||||
|  | @ -1071,8 +1067,8 @@ export default class SpecialVisualizations { | |||
|                     let challenge = Stores.FromPromise( | ||||
|                         Utils.downloadJsonCached( | ||||
|                             `${Maproulette.defaultEndpoint}/challenge/${parentId}`, | ||||
|                             24 * 60 * 60 * 1000 | ||||
|                         ) | ||||
|                             24 * 60 * 60 * 1000, | ||||
|                         ), | ||||
|                     ) | ||||
| 
 | ||||
|                     return new VariableUiElement( | ||||
|  | @ -1097,7 +1093,7 @@ export default class SpecialVisualizations { | |||
|                             } else { | ||||
|                                 return [title, new List(listItems)] | ||||
|                             } | ||||
|                         }) | ||||
|                         }), | ||||
|                     ) | ||||
|                 }, | ||||
|                 docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.", | ||||
|  | @ -1111,15 +1107,15 @@ export default class SpecialVisualizations { | |||
|                     "\n" + | ||||
|                     "```json\n" + | ||||
|                     "{\n" + | ||||
|                     '   "id": "mark_duplicate",\n' + | ||||
|                     '   "render": {\n' + | ||||
|                     '      "special": {\n' + | ||||
|                     '         "type": "maproulette_set_status",\n' + | ||||
|                     '         "message": {\n' + | ||||
|                     '            "en": "Mark as not found or false positive"\n' + | ||||
|                     "   \"id\": \"mark_duplicate\",\n" + | ||||
|                     "   \"render\": {\n" + | ||||
|                     "      \"special\": {\n" + | ||||
|                     "         \"type\": \"maproulette_set_status\",\n" + | ||||
|                     "         \"message\": {\n" + | ||||
|                     "            \"en\": \"Mark as not found or false positive\"\n" + | ||||
|                     "         },\n" + | ||||
|                     '         "status": "2",\n' + | ||||
|                     '         "image": "close"\n' + | ||||
|                     "         \"status\": \"2\",\n" + | ||||
|                     "         \"image\": \"close\"\n" + | ||||
|                     "      }\n" + | ||||
|                     "   }\n" + | ||||
|                     "}\n" + | ||||
|  | @ -1185,8 +1181,8 @@ export default class SpecialVisualizations { | |||
|                                     const fsBboxed = new BBoxFeatureSourceForLayer(fs, bbox) | ||||
|                                     return new StatisticsPanel(fsBboxed) | ||||
|                                 }, | ||||
|                                 [state.mapProperties.bounds] | ||||
|                             ) | ||||
|                                 [state.mapProperties.bounds], | ||||
|                             ), | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|  | @ -1252,7 +1248,7 @@ export default class SpecialVisualizations { | |||
|                 constr( | ||||
|                     state: SpecialVisualizationState, | ||||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     args: string[] | ||||
|                     args: string[], | ||||
|                 ): BaseUIElement { | ||||
|                     let [text, href, classnames, download, ariaLabel] = args | ||||
|                     if (download === "") { | ||||
|  | @ -1269,15 +1265,14 @@ export default class SpecialVisualizations { | |||
|                                     download: Utils.SubstituteKeys(download, tags), | ||||
|                                     ariaLabel: Utils.SubstituteKeys(ariaLabel, tags), | ||||
|                                     newTab, | ||||
|                                 }) | ||||
|                         ) | ||||
|                                 }), | ||||
|                         ), | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|             { | ||||
|                 funcName: "multi", | ||||
|                 docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering", | ||||
| 
 | ||||
|                 example: | ||||
|                     "```json\n" + | ||||
|                     JSON.stringify( | ||||
|  | @ -1293,7 +1288,7 @@ export default class SpecialVisualizations { | |||
|                             }, | ||||
|                         }, | ||||
|                         null, | ||||
|                         "  " | ||||
|                         "  ", | ||||
|                     ) + | ||||
|                     "\n```", | ||||
|                 args: [ | ||||
|  | @ -1313,18 +1308,28 @@ export default class SpecialVisualizations { | |||
|                     const translation = new Translation({ "*": tr }) | ||||
|                     return new VariableUiElement( | ||||
|                         featureTags.map((tags) => { | ||||
|                             const properties: object[] = JSON.parse(tags[key]) | ||||
|                             const elements = [] | ||||
|                             for (const property of properties) { | ||||
|                                 const subsTr = new SubstitutedTranslation( | ||||
|                                     translation, | ||||
|                                     new UIEventSource<any>(property), | ||||
|                                     state | ||||
|                                 ) | ||||
|                                 elements.push(subsTr) | ||||
|                             try { | ||||
|                                 const data = tags[key] | ||||
|                                 const properties: object[] = typeof data === "string" ? JSON.parse(tags[key]) : data | ||||
|                                 const elements = [] | ||||
|                                 for (const property of properties) { | ||||
|                                     const subsTr = new SubstitutedTranslation( | ||||
|                                         translation, | ||||
|                                         new UIEventSource<any>(property), | ||||
|                                         state, | ||||
|                                     ) | ||||
|                                     elements.push(subsTr) | ||||
|                                 } | ||||
|                                 return new List(elements) | ||||
|                             } catch (e) { | ||||
|                                 console.log("Something went wrong while generating the elements for a multi", { | ||||
|                                     e, | ||||
|                                     tags, | ||||
|                                     key, | ||||
|                                     loaded: tags[key], | ||||
|                                 }) | ||||
|                             } | ||||
|                             return new List(elements) | ||||
|                         }) | ||||
|                         }), | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|  | @ -1344,7 +1349,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     return new VariableUiElement( | ||||
|                         tagSource.map((tags) => { | ||||
|  | @ -1356,7 +1361,7 @@ export default class SpecialVisualizations { | |||
|                                 console.error("Cannot create a translation for", v, "due to", e) | ||||
|                                 return JSON.stringify(v) | ||||
|                             } | ||||
|                         }) | ||||
|                         }), | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|  | @ -1376,7 +1381,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     const key = argument[0] | ||||
|                     const validator = new FediverseValidator() | ||||
|  | @ -1386,14 +1391,14 @@ export default class SpecialVisualizations { | |||
|                             .map((fediAccount) => { | ||||
|                                 fediAccount = validator.reformat(fediAccount) | ||||
|                                 const [_, username, host] = fediAccount.match( | ||||
|                                     FediverseValidator.usernameAtServer | ||||
|                                     FediverseValidator.usernameAtServer, | ||||
|                                 ) | ||||
|                                 return new SvelteUIElement(Link, { | ||||
|                                     text: fediAccount, | ||||
|                                     url: "https://" + host + "/@" + username, | ||||
|                                     newTab: true, | ||||
|                                 }) | ||||
|                             }) | ||||
|                             }), | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|  | @ -1413,7 +1418,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     args: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     return new FixedUiElement("{" + args[0] + "}") | ||||
|                 }, | ||||
|  | @ -1434,7 +1439,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     const key = argument[0] ?? "value" | ||||
|                     return new VariableUiElement( | ||||
|  | @ -1452,12 +1457,12 @@ export default class SpecialVisualizations { | |||
|                             } catch (e) { | ||||
|                                 return new FixedUiElement( | ||||
|                                     "Could not parse this tag: " + | ||||
|                                         JSON.stringify(value) + | ||||
|                                         " due to " + | ||||
|                                         e | ||||
|                                     JSON.stringify(value) + | ||||
|                                     " due to " + | ||||
|                                     e, | ||||
|                                 ).SetClass("alert") | ||||
|                             } | ||||
|                         }) | ||||
|                         }), | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|  | @ -1478,7 +1483,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     const giggityUrl = argument[0] | ||||
|                     return new SvelteUIElement(Giggity, { tags: tagSource, state, giggityUrl }) | ||||
|  | @ -1494,12 +1499,12 @@ export default class SpecialVisualizations { | |||
|                     _: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     const tags = (<ThemeViewState>( | ||||
|                         state | ||||
|                     )).geolocation.currentUserLocation.features.map( | ||||
|                         (features) => features[0]?.properties | ||||
|                         (features) => features[0]?.properties, | ||||
|                     ) | ||||
|                     return new Combine([ | ||||
|                         new SvelteUIElement(OrientationDebugPanel, {}), | ||||
|  | @ -1521,7 +1526,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     return new SvelteUIElement(MarkAsFavourite, { | ||||
|                         tags: tagSource, | ||||
|  | @ -1541,7 +1546,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     return new SvelteUIElement(MarkAsFavouriteMini, { | ||||
|                         tags: tagSource, | ||||
|  | @ -1561,7 +1566,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     return new SvelteUIElement(DirectionIndicator, { state, feature }) | ||||
|                 }, | ||||
|  | @ -1576,7 +1581,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     argument: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     return new VariableUiElement( | ||||
|                         tagSource | ||||
|  | @ -1598,9 +1603,9 @@ export default class SpecialVisualizations { | |||
|                                     `${window.location.protocol}//${window.location.host}${window.location.pathname}?${layout}lat=${lat}&lon=${lon}&z=15` + | ||||
|                                     `#${id}` | ||||
|                                 return new Img(new Qr(url).toImageElement(75)).SetStyle( | ||||
|                                     "width: 75px" | ||||
|                                     "width: 75px", | ||||
|                                 ) | ||||
|                             }) | ||||
|                             }), | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|  | @ -1620,7 +1625,7 @@ export default class SpecialVisualizations { | |||
|                     tagSource: UIEventSource<Record<string, string>>, | ||||
|                     args: string[], | ||||
|                     feature: Feature, | ||||
|                     layer: LayerConfig | ||||
|                     layer: LayerConfig, | ||||
|                 ): BaseUIElement { | ||||
|                     const key = args[0] === "" ? "_direction:centerpoint" : args[0] | ||||
|                     return new VariableUiElement( | ||||
|  | @ -1631,41 +1636,55 @@ export default class SpecialVisualizations { | |||
|                             }) | ||||
|                             .mapD((value) => { | ||||
|                                 const dir = GeoOperations.bearingToHuman( | ||||
|                                     GeoOperations.parseBearing(value) | ||||
|                                     GeoOperations.parseBearing(value), | ||||
|                                 ) | ||||
|                                 console.log("Human dir", dir) | ||||
|                                 return Translations.t.general.visualFeedback.directionsAbsolute[dir] | ||||
|                             }) | ||||
|                             }), | ||||
|                     ) | ||||
|                 }, | ||||
|             }, | ||||
|             { | ||||
|                 funcName: "compare_data", | ||||
|                 needsUrls: (args) => args[1].split(";"), | ||||
|                 args:[ | ||||
|                 args: [ | ||||
|                     { | ||||
|                         name: "url", | ||||
|                         required: true, | ||||
|                         doc: "The attribute containing the url where to fetch more data" | ||||
|                         doc: "The attribute containing the url where to fetch more data", | ||||
|                     }, | ||||
|                     { | ||||
|                         name:"host", | ||||
|                         required:true, | ||||
|                         doc: "The domain name(s) where data might be fetched from - this is needed to set the CSP. A domain must include 'https', e.g. 'https://example.com'. For multiple domains, separate them with ';'. If you don't know the possible domains, use '*'. " | ||||
|                         name: "host", | ||||
|                         required: true, | ||||
|                         doc: "The domain name(s) where data might be fetched from - this is needed to set the CSP. A domain must include 'https', e.g. 'https://example.com'. For multiple domains, separate them with ';'. If you don't know the possible domains, use '*'. ", | ||||
|                     }, | ||||
|                     { | ||||
|                         name: "postprocessing", | ||||
|                         required: false, | ||||
|                         doc: "Apply some postprocessing. Currently, only 'velopark' is allowed as value" | ||||
|                         doc: "Apply some postprocessing. Currently, only 'velopark' is allowed as value", | ||||
|                     }, | ||||
|                     { | ||||
|                         name:"readonly", | ||||
|                         required: false, | ||||
|                         doc: "If 'yes', will not show 'apply'-buttons" | ||||
|                     } | ||||
|                 ], | ||||
|                 docs: "Gives an interactive element which shows a tag comparison between the OSM-object and the upstream object. This allows to copy some or all tags into OSM", | ||||
|                 constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, args: string[], feature: Feature, layer: LayerConfig): BaseUIElement { | ||||
|                     const url = args[0] | ||||
|                     const postprocessVelopark = args[2] === "velopark" | ||||
|                     return new SvelteUIElement(ComparisonTool, {url, postprocessVelopark, state, tags: tagSource, layer, feature}) | ||||
|                 } | ||||
|             } | ||||
|                     const readonly = args[3] === "yes" | ||||
|                     return new SvelteUIElement(ComparisonTool, { | ||||
|                         url, | ||||
|                         postprocessVelopark, | ||||
|                         state, | ||||
|                         tags: tagSource, | ||||
|                         layer, | ||||
|                         feature, | ||||
|                         readonly | ||||
|                     }) | ||||
|                 }, | ||||
|             }, | ||||
|         ] | ||||
| 
 | ||||
|         specialVisualizations.push(new AutoApplyButton(specialVisualizations)) | ||||
|  | @ -1677,7 +1696,7 @@ export default class SpecialVisualizations { | |||
|             throw ( | ||||
|                 "Invalid special visualisation found: funcName is undefined for " + | ||||
|                 invalid.map((sp) => sp.i).join(", ") + | ||||
|                 '. Did you perhaps type \n  funcName: "funcname" // type declaration uses COLON\ninstead of:\n  funcName = "funcName" // value definition uses EQUAL' | ||||
|                 ". Did you perhaps type \n  funcName: \"funcname\" // type declaration uses COLON\ninstead of:\n  funcName = \"funcName\" // value definition uses EQUAL" | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue