forked from MapComplete/MapComplete
		
	Fix import flow, add typing
This commit is contained in:
		
							parent
							
								
									97334fc369
								
							
						
					
					
						commit
						4246221e8e
					
				
					 29 changed files with 396 additions and 309 deletions
				
			
		|  | @ -3,6 +3,7 @@ import {ImmutableStore, Store, UIEventSource} from "../../UIEventSource"; | |||
| import {stat} from "fs"; | ||||
| import FilteredLayer from "../../../Models/FilteredLayer"; | ||||
| import {BBox} from "../../BBox"; | ||||
| import {Feature} from "@turf/turf"; | ||||
| 
 | ||||
| /** | ||||
|  * A simple, read only feature store. | ||||
|  | @ -11,7 +12,7 @@ export default class StaticFeatureSource implements FeatureSource { | |||
|     public readonly features: Store<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name: string | ||||
| 
 | ||||
|     constructor(features: Store<{ feature: any, freshness: Date }[]>, name = "StaticFeatureSource") { | ||||
|     constructor(features: Store<{ feature: Feature, freshness: Date }[]>, name = "StaticFeatureSource") { | ||||
|         if (features === undefined) { | ||||
|             throw "Static feature source received undefined as source" | ||||
|         } | ||||
|  | @ -19,17 +20,23 @@ export default class StaticFeatureSource implements FeatureSource { | |||
|         this.features = features; | ||||
|     } | ||||
| 
 | ||||
|     public static fromGeojsonAndDate(features: { feature: any, freshness: Date }[], name = "StaticFeatureSourceFromGeojsonAndDate"): StaticFeatureSource { | ||||
|     public static fromGeojsonAndDate(features: { feature: Feature, freshness: Date }[], name = "StaticFeatureSourceFromGeojsonAndDate"): StaticFeatureSource { | ||||
|         return new StaticFeatureSource(new ImmutableStore(features), name); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static fromGeojson(geojson: any[], name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource { | ||||
|     public static fromGeojson(geojson: Feature[], name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource { | ||||
|         const now = new Date(); | ||||
|         return StaticFeatureSource.fromGeojsonAndDate(geojson.map(feature => ({feature, freshness: now})), name); | ||||
|     } | ||||
| 
 | ||||
|     static fromDateless(featureSource: Store<{ feature: any }[]>, name = "StaticFeatureSourceFromDateless") { | ||||
|     public static fromGeojsonStore(geojson: Store<Feature[]>, name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource { | ||||
|         const now = new Date(); | ||||
|         const mapped : Store<{feature: Feature, freshness: Date}[]> = geojson.map(features => features.map(feature => ({feature, freshness: now}))) | ||||
|         return new StaticFeatureSource(mapped, name); | ||||
|     } | ||||
| 
 | ||||
|     static fromDateless(featureSource: Store<{ feature: Feature }[]>, name = "StaticFeatureSourceFromDateless") { | ||||
|         const now = new Date(); | ||||
|         return new StaticFeatureSource(featureSource.map(features => features.map(feature => ({ | ||||
|             feature: feature.feature, | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; | |||
| import {Or} from "../../Tags/Or"; | ||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | ||||
| import {OsmObject} from "../../Osm/OsmObject"; | ||||
| import {FeatureCollection} from "@turf/turf"; | ||||
| 
 | ||||
| /** | ||||
|  * If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile' | ||||
|  | @ -136,7 +137,7 @@ export default class OsmFeatureSource { | |||
| 
 | ||||
|                 console.log("Got tile", z, x, y, "from the osm api") | ||||
|                 this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y))) | ||||
|                 const geojson = OsmToGeoJson.default(osmJson, | ||||
|                 const geojson = <FeatureCollection<any , {id: string}>> OsmToGeoJson.default(osmJson, | ||||
|                     // @ts-ignore
 | ||||
|                     { | ||||
|                         flatProperties: true | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ import ChangeTagAction from "./ChangeTagAction"; | |||
| import {And} from "../../Tags/And"; | ||||
| import {Utils} from "../../../Utils"; | ||||
| import {OsmConnection} from "../OsmConnection"; | ||||
| import {GeoJSONObject} from "@turf/turf"; | ||||
| import {Feature} from "@turf/turf"; | ||||
| import FeaturePipeline from "../../FeatureSource/FeaturePipeline"; | ||||
| 
 | ||||
| export default class ReplaceGeometryAction extends OsmChangeAction { | ||||
|  | @ -83,7 +83,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|     // noinspection JSUnusedGlobalSymbols
 | ||||
|     public async getPreview(): Promise<FeatureSource> { | ||||
|         const {closestIds, allNodesById, detachedNodes, reprojectedNodes} = await this.GetClosestIds(); | ||||
|         const preview: GeoJSONObject[] = closestIds.map((newId, i) => { | ||||
|         const preview: Feature[] = closestIds.map((newId, i) => { | ||||
|             if (this.identicalTo[i] !== undefined) { | ||||
|                 return undefined | ||||
|             } | ||||
|  | @ -122,7 +122,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
|         reprojectedNodes.forEach(({newLat, newLon, nodeId}) => { | ||||
| 
 | ||||
|             const origNode = allNodesById.get(nodeId); | ||||
|             const feature = { | ||||
|             const feature : Feature =  { | ||||
|                 type: "Feature", | ||||
|                 properties: { | ||||
|                     "move": "yes", | ||||
|  | @ -142,7 +142,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { | |||
| 
 | ||||
|         detachedNodes.forEach(({reason}, id) => { | ||||
|             const origNode = allNodesById.get(id); | ||||
|             const feature = { | ||||
|             const feature : Feature = { | ||||
|                 type: "Feature", | ||||
|                 properties: { | ||||
|                     "detach": "yes", | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ export class Overpass { | |||
|         this._relationTracker = relationTracker | ||||
|     } | ||||
| 
 | ||||
|     public async queryGeoJson(bounds: BBox, ): Promise<[FeatureCollection, Date]> { | ||||
|     public async queryGeoJson(bounds: BBox): Promise<[FeatureCollection, Date]> { | ||||
|         const bbox = "[bbox:" + bounds.getSouth() + "," + bounds.getWest() + "," + bounds.getNorth() + "," + bounds.getEast() + "]"; | ||||
|         const query = this.buildScript(bbox) | ||||
|         return this.ExecuteQuery(query); | ||||
|  |  | |||
|  | @ -7,8 +7,12 @@ import {Utils} from "../../Utils"; | |||
|  */ | ||||
| export class IdbLocalStorage { | ||||
| 
 | ||||
| 
 | ||||
|     private static readonly _sourceCache: Record<string, UIEventSource<any>> = {} | ||||
|      | ||||
|     public static Get<T>(key: string, options?: { defaultValue?: T, whenLoaded?: (t: T | null) => void }): UIEventSource<T> { | ||||
|         if(IdbLocalStorage._sourceCache[key] !== undefined){ | ||||
|             return IdbLocalStorage._sourceCache[key] | ||||
|         } | ||||
|         const src = new UIEventSource<T>(options?.defaultValue, "idb-local-storage:" + key) | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return src; | ||||
|  | @ -26,6 +30,7 @@ export class IdbLocalStorage { | |||
|                 options?.whenLoaded(null) | ||||
|             } | ||||
|         }) | ||||
|         IdbLocalStorage._sourceCache[key] = src; | ||||
|         return src; | ||||
| 
 | ||||
|     } | ||||
|  |  | |||
|  | @ -75,8 +75,8 @@ export class DoesImageExist extends DesugaringStep<string> { | |||
|                 return {result: image}; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (this._knownImagePaths !== undefined && !this._knownImagePaths.has(image)) { | ||||
|          | ||||
|         if (!this._knownImagePaths.has(image)) { | ||||
|             if (this.doesPathExist === undefined) { | ||||
|                 errors.push(`Image with path ${image} not found or not attributed; it is used in ${context}`) | ||||
|             } else if (!this.doesPathExist(image)) { | ||||
|  |  | |||
|  | @ -28,7 +28,10 @@ export default class ScrollableFullScreen extends UIElement { | |||
|     constructor(title: ((options: { mode: string }) => BaseUIElement), | ||||
|                 content: ((options: { mode: string, resetScrollSignal: UIEventSource<void> }) => BaseUIElement), | ||||
|                 hashToShow: string, | ||||
|                 isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|                 isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false), | ||||
|                 options?: { | ||||
|                     setHash?: true | boolean     | ||||
|                 } | ||||
|     ) { | ||||
|         super(); | ||||
|         this.hashToShow = hashToShow; | ||||
|  | @ -53,16 +56,21 @@ export default class ScrollableFullScreen extends UIElement { | |||
| 
 | ||||
| 
 | ||||
|         const self = this; | ||||
|         Hash.hash.addCallback(h => { | ||||
|             if (h === undefined) { | ||||
|                 isShown.setData(false) | ||||
|             } | ||||
|         }) | ||||
|         const setHash = options?.setHash ?? true; | ||||
|         if(setHash){ | ||||
|             Hash.hash.addCallback(h => { | ||||
|                 if (h === undefined) { | ||||
|                     isShown.setData(false) | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
|         isShown.addCallback(isShown => { | ||||
|             if (isShown) { | ||||
|                 // We first must set the hash, then activate the panel
 | ||||
|                 // If the order is wrong, this will cause the panel to disactivate again
 | ||||
|                 Hash.hash.setData(hashToShow) | ||||
|                 if(setHash){ | ||||
|                     Hash.hash.setData(hashToShow) | ||||
|                 } | ||||
|                 self.Activate(); | ||||
|             } else { | ||||
|                 // Some cleanup...
 | ||||
|  |  | |||
|  | @ -159,6 +159,12 @@ class SingleLayerSelectionButton extends Toggle { | |||
| 
 | ||||
| export default class BackgroundMapSwitch extends Combine { | ||||
| 
 | ||||
|     /** | ||||
|      * Three buttons to easily switch map layers between OSM, aerial and some map. | ||||
|      * @param state | ||||
|      * @param currentBackground | ||||
|      * @param options | ||||
|      */ | ||||
|     constructor( | ||||
|         state: { | ||||
|             locationControl: UIEventSource<Loc>, | ||||
|  |  | |||
|  | @ -48,7 +48,10 @@ export default class LeftControls extends Combine { | |||
|                     } | ||||
|                     return new Lazy(() => { | ||||
|                         const tagsSource = state.allElements.getEventSourceById(feature.properties.id) | ||||
|                         return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, state, "currentview", guiState.currentViewControlIsOpened) | ||||
|                         return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, state, { | ||||
|                             hashToShow: "currentview", | ||||
|                             isShown: guiState.currentViewControlIsOpened | ||||
|                         }) | ||||
|                             .SetClass("md:floating-element-width") | ||||
|                     }) | ||||
|                 })).SetStyle("width: 40rem").SetClass("block") | ||||
|  |  | |||
|  | @ -22,12 +22,17 @@ import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | |||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import ValidatedTextField from "../Input/ValidatedTextField"; | ||||
| import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource"; | ||||
| import * as currentview from "../../assets/layers/current_view/current_view.json" | ||||
| import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json" | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import FeatureInfoBox from "../Popup/FeatureInfoBox"; | ||||
| import {ImportUtils} from "./ImportUtils"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; | ||||
| import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; | ||||
| import {Feature, FeatureCollection} from "@turf/turf"; | ||||
| import * as currentview from "../../assets/layers/current_view/current_view.json" | ||||
| import {CheckBox} from "../Input/Checkboxes"; | ||||
| import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"; | ||||
| 
 | ||||
| /** | ||||
|  * Given the data to import, the bbox and the layer, will query overpass for similar items | ||||
|  | @ -36,20 +41,21 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
| 
 | ||||
|     public readonly IsValid | ||||
|     public readonly Value: Store<{ features: any[], theme: string }> | ||||
| 
 | ||||
|      | ||||
|     constructor( | ||||
|         state, | ||||
|         params: { bbox: BBox, layer: LayerConfig, theme: string, features: any[] }) { | ||||
| 
 | ||||
|         const t = Translations.t.importHelper.conflationChecker | ||||
| 
 | ||||
|         const bbox = params.bbox.padAbsolute(0.0001) | ||||
|         const layer = params.layer; | ||||
|         const toImport: {features: any[]} = params; | ||||
|          | ||||
|         const toImport: { features: any[] } = params; | ||||
|         let overpassStatus = new UIEventSource<{ error: string } | "running" | "success" | "idle" | "cached">("idle") | ||||
|         const cacheAge = new UIEventSource<number>(undefined); | ||||
|          | ||||
|          | ||||
|         function loadDataFromOverpass(){ | ||||
| 
 | ||||
| 
 | ||||
|         function loadDataFromOverpass() { | ||||
|             // Load the data!
 | ||||
|             const url = Constants.defaultOverpassUrls[1] | ||||
|             const relationTracker = new RelationsTracker() | ||||
|  | @ -66,42 +72,49 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|                     overpassStatus.setData({error}) | ||||
|                 }) | ||||
|         } | ||||
|          | ||||
|          | ||||
| 
 | ||||
| 
 | ||||
|         const fromLocalStorage = IdbLocalStorage.Get<[any, Date]>("importer-overpass-cache-" + layer.id, { | ||||
|              | ||||
| 
 | ||||
|             whenLoaded: (v) => { | ||||
|                 if (v !== undefined && v !== null) { | ||||
|                     console.log("Loaded from local storage:", v) | ||||
|                     const [geojson, date] = v; | ||||
|                     const timeDiff = (new Date().getTime() - date.getTime()) / 1000; | ||||
|                     console.log("Loaded ", geojson.features.length, " features; cache is ", timeDiff, "seconds old") | ||||
|                     cacheAge.setData(timeDiff) | ||||
|                     if (timeDiff < 24 * 60 * 60) { | ||||
|                         // Recently cached! 
 | ||||
|                         overpassStatus.setData("cached") | ||||
|                         return; | ||||
|                     } | ||||
|                     cacheAge.setData(-1) | ||||
|                     overpassStatus.setData("cached") | ||||
|                 } | ||||
|                 loadDataFromOverpass() | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         const cacheAge = fromLocalStorage.map(d => { | ||||
|             if(d === undefined || d[1] === undefined){ | ||||
|                 return undefined | ||||
|             } | ||||
|             const [_, loadedDate] = d | ||||
|             return (new Date().getTime() - loadedDate.getTime()) / 1000; | ||||
|         }) | ||||
|         cacheAge.addCallbackD(timeDiff => { | ||||
|             if (timeDiff < 24 * 60 * 60) { | ||||
|                 // Recently cached! 
 | ||||
|                 overpassStatus.setData("cached") | ||||
|                 return; | ||||
|             } else { | ||||
|                 loadDataFromOverpass() | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         const geojson: Store<any> = fromLocalStorage.map(d => { | ||||
|         const geojson: Store<FeatureCollection> = fromLocalStorage.map(d => { | ||||
|             if (d === undefined) { | ||||
|                 return undefined | ||||
|             } | ||||
|             return d[0] | ||||
|         }) | ||||
| 
 | ||||
|          | ||||
|         const background = new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto) | ||||
|         const location = new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1}) | ||||
|         const currentBounds = new UIEventSource<BBox>(undefined) | ||||
|         const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement() | ||||
|         const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement({ | ||||
|             value: LocalStorageSource.GetParsed<string>("importer-zoom-level", "0") | ||||
|         }) | ||||
|         zoomLevel.SetClass("ml-1 border border-black") | ||||
|         zoomLevel.GetValue().syncWith(LocalStorageSource.Get("importer-zoom-level", "14"), true) | ||||
|         const osmLiveData = Minimap.createMiniMap({ | ||||
|             allowMoving: true, | ||||
|             location, | ||||
|  | @ -110,18 +123,24 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|             attribution: new Attribution(location, state.osmConnection.userDetails, undefined, currentBounds) | ||||
|         }) | ||||
|         osmLiveData.SetClass("w-full").SetStyle("height: 500px") | ||||
|         const preview = new StaticFeatureSource(geojson.map(geojson => { | ||||
|          | ||||
|         const geojsonFeatures : Store<Feature[]> = geojson.map(geojson => { | ||||
|             if (geojson?.features === undefined) { | ||||
|                 return [] | ||||
|             } | ||||
|             const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(zoomLevel.GetValue().data) | ||||
|             if (!zoomedEnough) { | ||||
|             const currentZoom = zoomLevel.GetValue().data | ||||
|             const zoomedEnough: boolean = osmLiveData.location.data.zoom >= Number(currentZoom) | ||||
|             if (currentZoom !== undefined && !zoomedEnough) { | ||||
|                 return [] | ||||
|             } | ||||
|             const bounds = osmLiveData.bounds.data | ||||
|             if(bounds === undefined){ | ||||
|                 return geojson.features; | ||||
|             } | ||||
|             return geojson.features.filter(f => BBox.get(f).overlapsWith(bounds)) | ||||
|         }, [osmLiveData.bounds, zoomLevel.GetValue()])); | ||||
| 
 | ||||
|         }, [osmLiveData.bounds, zoomLevel.GetValue()]) | ||||
|          | ||||
|         const preview = StaticFeatureSource.fromGeojsonStore(geojsonFeatures) | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow: new LayerConfig(currentview), | ||||
|  | @ -134,12 +153,16 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|             ]) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow: layer, | ||||
|         new ShowDataMultiLayer({ | ||||
|             //layerToShow: layer,
 | ||||
|             layers: new UIEventSource<FilteredLayer[]>([{ | ||||
|                 layerDef: layer, | ||||
|                 isDisplayed: new UIEventSource<boolean>(true), | ||||
|                 appliedFilters: new UIEventSource<Map<string, FilterState>>(undefined) | ||||
|             }]), | ||||
|             state, | ||||
|             leafletMap: osmLiveData.leafletMap, | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}), | ||||
|             zoomToFeatures: false, | ||||
|             features: preview | ||||
|         }) | ||||
|  | @ -148,7 +171,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|             layerToShow: new LayerConfig(import_candidate), | ||||
|             state, | ||||
|             leafletMap: osmLiveData.leafletMap, | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}), | ||||
|             zoomToFeatures: false, | ||||
|             features: StaticFeatureSource.fromGeojson(toImport.features) | ||||
|         }) | ||||
|  | @ -164,7 +187,7 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|         matchedFeaturesMap.SetClass("w-full").SetStyle("height: 500px") | ||||
| 
 | ||||
|         // Featuresource showing OSM-features which are nearby a toImport-feature 
 | ||||
|         const nearbyFeatures = new StaticFeatureSource(geojson.map(osmData => { | ||||
|         const geojsonMapped: Store<Feature[]> = geojson.map(osmData => { | ||||
|             if (osmData?.features === undefined) { | ||||
|                 return [] | ||||
|             } | ||||
|  | @ -172,32 +195,37 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|             return osmData.features.filter(f => | ||||
|                 toImport.features.some(imp => | ||||
|                     maxDist >= GeoOperations.distanceBetween(imp.geometry.coordinates, GeoOperations.centerpointCoordinates(f)))) | ||||
|         }, [nearbyCutoff.GetValue().stabilized(500)])); | ||||
|         }, [nearbyCutoff.GetValue().stabilized(500)]) | ||||
|         const nearbyFeatures = StaticFeatureSource.fromGeojsonStore(geojsonMapped); | ||||
|         const paritionedImport = ImportUtils.partitionFeaturesIfNearby(toImport, geojson, nearbyCutoff.GetValue().map(Number)); | ||||
| 
 | ||||
|         // Featuresource showing OSM-features which are nearby a toImport-feature 
 | ||||
|         const toImportWithNearby = new StaticFeatureSource(paritionedImport.map(els => els?.hasNearby ?? [])); | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow: layer, | ||||
|             state, | ||||
|             leafletMap: matchedFeaturesMap.leafletMap, | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), | ||||
|             zoomToFeatures: true, | ||||
|             features: nearbyFeatures | ||||
|         }) | ||||
|         const toImportWithNearby = StaticFeatureSource.fromGeojsonStore(paritionedImport.map(els => els?.hasNearby ?? [])); | ||||
|         toImportWithNearby.features.addCallback(nearby => console.log("The following features are near an already existing object:", nearby)) | ||||
| 
 | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow: new LayerConfig(import_candidate), | ||||
|             state, | ||||
|             leafletMap: matchedFeaturesMap.leafletMap, | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state), | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}), | ||||
|             zoomToFeatures: false, | ||||
|             features: toImportWithNearby | ||||
|         }) | ||||
|         const showOsmLayer = new CheckBox(t.showOsmLayerInConflationMap, true) | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow: layer, | ||||
|             state, | ||||
|             leafletMap: matchedFeaturesMap.leafletMap, | ||||
|             popup: (tags, layer) => new FeatureInfoBox(tags, layer, state, {setHash: false}), | ||||
|             zoomToFeatures: true, | ||||
|             features: nearbyFeatures, | ||||
|             doShowLayer: showOsmLayer.GetValue() | ||||
|         }) | ||||
| 
 | ||||
|         const t = Translations.t.importHelper.conflationChecker | ||||
| 
 | ||||
| 
 | ||||
|        | ||||
|       | ||||
|         const conflationMaps = new Combine([ | ||||
|             new VariableUiElement( | ||||
|                 geojson.map(geojson => { | ||||
|  | @ -218,34 +246,44 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
|                     return t.cacheExpired | ||||
|                 } | ||||
|                 return new Combine([t.loadedDataAge.Subs({age: Utils.toHumanTime(age)}), | ||||
|                 new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache) | ||||
|                     .onClick(loadDataFromOverpass) | ||||
|                     .SetClass("h-12") | ||||
|                     new SubtleButton(Svg.reload_svg().SetClass("h-8"), t.reloadTheCache) | ||||
|                         .onClick(loadDataFromOverpass) | ||||
|                         .SetClass("h-12") | ||||
|                 ]) | ||||
|             })), | ||||
| 
 | ||||
|             new Title(t.titleLive), | ||||
|             t.importCandidatesCount.Subs({count:toImport.features.length }), | ||||
|              new VariableUiElement(geojson.map(geojson => { | ||||
|                  if(geojson?.features?.length === undefined || geojson?.features?.length === 0){ | ||||
|             t.importCandidatesCount.Subs({count: toImport.features.length}), | ||||
|             new VariableUiElement(geojson.map(geojson => { | ||||
|                 if (geojson?.features?.length === undefined || geojson?.features?.length === 0) { | ||||
|                     return t.nothingLoaded.Subs(layer).SetClass("alert") | ||||
|                  } | ||||
|                  return new Combine([ | ||||
|                 } | ||||
|                 return new Combine([ | ||||
|                     t.osmLoaded.Subs({count: geojson.features.length, name: layer.name}), | ||||
|                       | ||||
|                  ])  | ||||
|              })), | ||||
| 
 | ||||
|                 ]) | ||||
|             })), | ||||
|             osmLiveData, | ||||
|             new VariableUiElement(osmLiveData.location.map(location => { | ||||
|                 return t.zoomIn.Subs({needed:zoomLevel, current: location.zoom }) | ||||
|             } )), | ||||
|             new Combine([ | ||||
|                 t.zoomLevelSelection, | ||||
|                 zoomLevel, | ||||
|                 new VariableUiElement(osmLiveData.location.map(location => { | ||||
|                     return t.zoomIn.Subs(<any>{current: location.zoom}) | ||||
|                 })), | ||||
|             ]).SetClass("flex"), | ||||
|             new Title(t.titleNearby), | ||||
|             new Combine([t.mapShowingNearbyIntro, nearbyCutoff]).SetClass("flex"), | ||||
|             new VariableUiElement(toImportWithNearby.features.map(feats =>  | ||||
|             new VariableUiElement(toImportWithNearby.features.map(feats => | ||||
|                 t.nearbyWarn.Subs({count: feats.length}).SetClass("alert"))), | ||||
|             t.setRangeToZero, | ||||
|             matchedFeaturesMap]).SetClass("flex flex-col") | ||||
| 
 | ||||
|             matchedFeaturesMap, | ||||
|             new Combine([ | ||||
|             new BackgroundMapSwitch({backgroundLayer: background, locationControl: matchedFeaturesMap.location}, background), | ||||
|             showOsmLayer, | ||||
|                  | ||||
|             ]).SetClass("flex") | ||||
|              | ||||
|         ]).SetClass("flex flex-col") | ||||
|         super([ | ||||
|             new Title(t.title), | ||||
|             new VariableUiElement(overpassStatus.map(d => { | ||||
|  | @ -270,7 +308,11 @@ export default class ConflationChecker extends Combine implements FlowStep<{ fea | |||
| 
 | ||||
|         ]) | ||||
| 
 | ||||
|         this.Value = paritionedImport.map(feats => ({theme: params.theme, features: feats?.noNearby, layer: params.layer})) | ||||
|         this.Value = paritionedImport.map(feats => ({ | ||||
|             theme: params.theme, | ||||
|             features: feats?.noNearby, | ||||
|             layer: params.layer | ||||
|         })) | ||||
|         this.IsValid = this.Value.map(v => v?.features !== undefined && v.features.length > 0) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,8 +1,13 @@ | |||
| import {Store} from "../../Logic/UIEventSource"; | ||||
| import {GeoOperations} from "../../Logic/GeoOperations"; | ||||
| import {Feature, Geometry} from "@turf/turf"; | ||||
| 
 | ||||
| export class ImportUtils { | ||||
|     public static partitionFeaturesIfNearby(toPartitionFeatureCollection: ({ features: any[] }), compareWith: Store<{ features: any[] }>, cutoffDistanceInMeters: Store<number>): Store<{ hasNearby: any[], noNearby: any[] }> { | ||||
|     public static partitionFeaturesIfNearby( | ||||
|         toPartitionFeatureCollection: ({ features: Feature<Geometry>[] }), | ||||
|         compareWith: Store<{ features: Feature[] }>, | ||||
|         cutoffDistanceInMeters: Store<number>) | ||||
|         : Store<{ hasNearby: Feature[], noNearby: Feature[] }> { | ||||
|         return compareWith.map(osmData => { | ||||
|             if (osmData?.features === undefined) { | ||||
|                 return undefined | ||||
|  | @ -16,7 +21,7 @@ export class ImportUtils { | |||
|             const noNearby = [] | ||||
|             for (const toImportElement of toPartitionFeatureCollection.features) { | ||||
|                 const hasNearbyFeature = osmData.features.some(f => | ||||
|                     maxDist >= GeoOperations.distanceBetween(toImportElement.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) | ||||
|                     maxDist >= GeoOperations.distanceBetween(<any> toImportElement.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) | ||||
|                 if (hasNearbyFeature) { | ||||
|                     hasNearby.push(toImportElement) | ||||
|                 } else { | ||||
|  |  | |||
|  | @ -24,6 +24,7 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | |||
| import Title from "../Base/Title"; | ||||
| import CheckBoxes from "../Input/Checkboxes"; | ||||
| import {AllTagsPanel} from "../AllTagsPanel"; | ||||
| import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"; | ||||
| 
 | ||||
| class PreviewPanel extends ScrollableFullScreen { | ||||
| 
 | ||||
|  | @ -109,6 +110,10 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: | |||
|             bounds: currentBounds, | ||||
|             attribution: new Attribution(location, state.osmConnection.userDetails, undefined, currentBounds) | ||||
|         }) | ||||
|         const layerControl =  new BackgroundMapSwitch( { | ||||
|             backgroundLayer: background, | ||||
|             locationControl: location | ||||
|         },background) | ||||
|         map.SetClass("w-full").SetStyle("height: 500px") | ||||
| 
 | ||||
|         new ShowDataMultiLayer({ | ||||
|  | @ -147,6 +152,7 @@ export class MapPreview extends Combine implements FlowStep<{ bbox: BBox, layer: | |||
| 
 | ||||
|             mismatchIndicator, | ||||
|             map, | ||||
|             layerControl, | ||||
|             confirm | ||||
|         ]); | ||||
| 
 | ||||
|  |  | |||
|  | @ -4,13 +4,13 @@ import {Utils} from "../../Utils"; | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import InputElementMap from "./InputElementMap"; | ||||
| 
 | ||||
| export  class CheckBox extends InputElementMap<number[], boolean> { | ||||
| export class CheckBox extends InputElementMap<number[], boolean> { | ||||
|     constructor(el: BaseUIElement , defaultValue?: boolean) { | ||||
|         super( | ||||
|             new CheckBoxes([el]), | ||||
|             (x0, x1) => x0 === x1, | ||||
|             t => t.length > 0, | ||||
|             x => x ? [0] : [] | ||||
|             x => x ? [0] : [], | ||||
|         ); | ||||
|         if(defaultValue !== undefined){ | ||||
|             this.GetValue().setData(defaultValue) | ||||
|  |  | |||
|  | @ -25,16 +25,20 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | |||
|         tags: UIEventSource<any>, | ||||
|         layerConfig: LayerConfig, | ||||
|         state: FeaturePipelineState, | ||||
|         hashToShow?: string, | ||||
|         isShown?: UIEventSource<boolean>, | ||||
|         options?: { | ||||
|             hashToShow?: string, | ||||
|             isShown?: UIEventSource<boolean>, | ||||
|             setHash?: true | boolean | ||||
|         } | ||||
|     ) { | ||||
|         if (state === undefined) { | ||||
|             throw "State is undefined!" | ||||
|         } | ||||
|         super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig, state), | ||||
|             () => FeatureInfoBox.GenerateContent(tags, layerConfig, state), | ||||
|             hashToShow ?? tags.data.id ?? "item", | ||||
|             isShown); | ||||
|             options?.hashToShow ?? tags.data.id ?? "item", | ||||
|             options?.isShown, | ||||
|             options); | ||||
| 
 | ||||
|         if (layerConfig === undefined) { | ||||
|             throw "Undefined layerconfig"; | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store, UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; | ||||
| import {ElementStorage} from "../../Logic/ElementStorage"; | ||||
|  | @ -20,7 +20,7 @@ We don't actually import it here. It is imported in the 'MinimapImplementation'- | |||
| export default class ShowDataLayerImplementation { | ||||
| 
 | ||||
|     private static dataLayerIds = 0 | ||||
|     private readonly _leafletMap: UIEventSource<L.Map>; | ||||
|     private readonly _leafletMap: Store<L.Map>; | ||||
|     private readonly _enablePopups: boolean; | ||||
|     private readonly _features: RenderingMultiPlexerFeatureSource | ||||
|     private readonly _layerToShow: LayerConfig; | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen"; | |||
| export interface ShowDataLayerOptions { | ||||
|     features: FeatureSource, | ||||
|     selectedElement?: UIEventSource<any>, | ||||
|     leafletMap: UIEventSource<L.Map>, | ||||
|     leafletMap: Store<L.Map>, | ||||
|     popup?: undefined | ((tags: UIEventSource<any>, layer: LayerConfig) => ScrollableFullScreen), | ||||
|     zoomToFeatures?: false | boolean, | ||||
|     doShowLayer?: Store<boolean>, | ||||
|  |  | |||
|  | @ -1,14 +1,14 @@ | |||
| /** | ||||
|  * SHows geojson on the given leaflet map, but attempts to figure out the correct layer first | ||||
|  */ | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {Store} from "../../Logic/UIEventSource"; | ||||
| import ShowDataLayer from "./ShowDataLayer"; | ||||
| import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; | ||||
| 
 | ||||
| export default class ShowDataMultiLayer { | ||||
|     constructor(options: ShowDataLayerOptions & { layers: UIEventSource<FilteredLayer[]> }) { | ||||
|     constructor(options: ShowDataLayerOptions & { layers: Store<FilteredLayer[]> }) { | ||||
| 
 | ||||
|         new PerLayerFeatureSourceSplitter(options.layers, (perLayer => { | ||||
|                 const newOptions = { | ||||
|  |  | |||
|  | @ -49,6 +49,9 @@ export class SubstitutedTranslation extends VariableUiElement { | |||
|                 const allElements = SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map( | ||||
|                     proto => { | ||||
|                         if (proto.fixed !== undefined) { | ||||
|                             if(tagsSource === undefined){ | ||||
|                                 return Utils.SubstituteKeys(proto.fixed, undefined) | ||||
|                             } | ||||
|                             return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags))); | ||||
|                         } | ||||
|                         const viz = proto.special; | ||||
|  |  | |||
							
								
								
									
										4
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -284,7 +284,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
|      * @param useLang | ||||
|      * @constructor | ||||
|      */ | ||||
|     public static SubstituteKeys(txt: string | undefined, tags: any, useLang?: string): string | undefined { | ||||
|     public static SubstituteKeys(txt: string | undefined, tags?: any, useLang?: string): string | undefined { | ||||
|         if (txt === undefined) { | ||||
|             return undefined | ||||
|         } | ||||
|  | @ -294,7 +294,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
| 
 | ||||
|         while (match) { | ||||
|             const key = match[1] | ||||
|             let v = tags[key] | ||||
|             let v = tags === undefined ? undefined : tags[key] | ||||
|             if (v !== undefined) { | ||||
| 
 | ||||
|                 if (v["toISOString"] != undefined) { | ||||
|  |  | |||
|  | @ -1,136 +1,138 @@ | |||
| { | ||||
|     "id": "doctors", | ||||
|     "name": { | ||||
|         "en": "doctors" | ||||
|   "id": "doctors", | ||||
|   "name": { | ||||
|     "en": "doctors" | ||||
|   }, | ||||
|   "source": { | ||||
|     "osmTags": { | ||||
|       "or": [ | ||||
|         "amenity=doctors", | ||||
|         "amenity=dentist", | ||||
|         "healthcare=physiotherapist" | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   "title": { | ||||
|     "render": { | ||||
|       "en": "Doctors Office {name}" | ||||
|     }, | ||||
|     "source": { | ||||
|         "osmTags": { | ||||
|             "or": [ | ||||
|                 "amenity=doctors", | ||||
|                 "amenity=dentist", | ||||
|                 "healthcare=physiotherapist" | ||||
|             ] | ||||
|         } | ||||
|     }, | ||||
|     "title": { | ||||
|         "render": { | ||||
|             "en": "Doctors Office {name}" | ||||
|         }, | ||||
|         "mappings": [ | ||||
|             { | ||||
|                 "if": "amenity=doctors", | ||||
|                 "then": "Doctors Office {name}" | ||||
|             }, | ||||
|             { | ||||
|                 "if": "amenity=dentist", | ||||
|                 "then": "Dentists office {name}" | ||||
|             }, | ||||
|             { | ||||
|                 "if": "healthcare=physiotherapist", | ||||
|                 "then": "Physiotherapists office {name}" | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     "minzoom": 13, | ||||
|     "tagRenderings": [ | ||||
|         "images", | ||||
|         "opening_hours", | ||||
|         "phone", | ||||
|         "email", | ||||
|         "website", | ||||
|         { | ||||
|             "condition": "amenity=doctors", | ||||
|             "id": "specialty", | ||||
|             "render": { | ||||
|                 "en": "This doctor is specialized in {healthcare:speciality}" | ||||
|             }, | ||||
|             "question": { | ||||
|                 "en": "What is this doctor specialized in?" | ||||
|             }, | ||||
|             "freeform": { | ||||
|                 "key": "healthcare:speciality" | ||||
|             }, | ||||
|             "mappings": [ | ||||
|                 { | ||||
|                     "if": "healthcare:speciality=general", | ||||
|                     "then": { | ||||
|                         "en": "This is a general practitioner" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "if": "healthcare:speciality=gynaecology", | ||||
|                     "then": { | ||||
|                         "en": "This is a gynaecologist" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "if": "healthcare:speciality=psychiatry", | ||||
|                     "then": { | ||||
|                         "en": "This is a psychiatrist" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "if": "healthcare:speciality=paediatrics", | ||||
|                     "then": { | ||||
|                         "en": "This is a paediatrician" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     ], | ||||
|     "presets": [ | ||||
|         { | ||||
|             "title": { | ||||
|                 "en": "a doctors office" | ||||
|             }, | ||||
|             "tags": [ | ||||
|                 "amenity=doctors" | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             "title": { | ||||
|                 "en": "a dentists office" | ||||
|             }, | ||||
|             "tags": [ | ||||
|                 "amenity=dentist" | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             "title": { | ||||
|                 "en": "a physiotherapists office" | ||||
|             }, | ||||
|             "tags": [ | ||||
|                 "healthcare=physiotherapist" | ||||
|             ] | ||||
|         } | ||||
|     ], | ||||
|     "filter": [ | ||||
|         { | ||||
|             "id": "opened-now", | ||||
|             "options": [ | ||||
|                 { | ||||
|                     "question": { | ||||
|                         "en": "Opened now" | ||||
|                     }, | ||||
|                     "osmTags": "_isOpen=yes" | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     ], | ||||
|     "mapRendering": [ | ||||
|         { | ||||
|             "icon": { | ||||
|                 "render": "circle:white;./assets/layers/doctors/doctors.svg", | ||||
|                 "mappings": [{ | ||||
|                     "if": "amenity=dentist", | ||||
|                     "then": "circle:white;./assets/layers/doctors/dentist.svg" | ||||
|                 }] | ||||
|             }, | ||||
|             "iconSize": "40,40,center", | ||||
|             "location": [ | ||||
|                 "point", | ||||
|                 "centroid" | ||||
|             ] | ||||
|         } | ||||
|     "mappings": [ | ||||
|       { | ||||
|         "if": "amenity=doctors", | ||||
|         "then": "Doctors Office {name}" | ||||
|       }, | ||||
|       { | ||||
|         "if": "amenity=dentist", | ||||
|         "then": "Dentists office {name}" | ||||
|       }, | ||||
|       { | ||||
|         "if": "healthcare=physiotherapist", | ||||
|         "then": "Physiotherapists office {name}" | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "minzoom": 13, | ||||
|   "tagRenderings": [ | ||||
|     "images", | ||||
|     "opening_hours", | ||||
|     "phone", | ||||
|     "email", | ||||
|     "website", | ||||
|     { | ||||
|       "condition": "amenity=doctors", | ||||
|       "id": "specialty", | ||||
|       "render": { | ||||
|         "en": "This doctor is specialized in {healthcare:speciality}" | ||||
|       }, | ||||
|       "question": { | ||||
|         "en": "What is this doctor specialized in?" | ||||
|       }, | ||||
|       "freeform": { | ||||
|         "key": "healthcare:speciality" | ||||
|       }, | ||||
|       "mappings": [ | ||||
|         { | ||||
|           "if": "healthcare:speciality=general", | ||||
|           "then": { | ||||
|             "en": "This is a general practitioner" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "if": "healthcare:speciality=gynaecology", | ||||
|           "then": { | ||||
|             "en": "This is a gynaecologist" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "if": "healthcare:speciality=psychiatry", | ||||
|           "then": { | ||||
|             "en": "This is a psychiatrist" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "if": "healthcare:speciality=paediatrics", | ||||
|           "then": { | ||||
|             "en": "This is a paediatrician" | ||||
|           } | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "presets": [ | ||||
|     { | ||||
|       "title": { | ||||
|         "en": "a doctors office" | ||||
|       }, | ||||
|       "tags": [ | ||||
|         "amenity=doctors" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "title": { | ||||
|         "en": "a dentists office" | ||||
|       }, | ||||
|       "tags": [ | ||||
|         "amenity=dentist" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "title": { | ||||
|         "en": "a physiotherapists office" | ||||
|       }, | ||||
|       "tags": [ | ||||
|         "healthcare=physiotherapist" | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "filter": [ | ||||
|     { | ||||
|       "id": "opened-now", | ||||
|       "options": [ | ||||
|         { | ||||
|           "question": { | ||||
|             "en": "Opened now" | ||||
|           }, | ||||
|           "osmTags": "_isOpen=yes" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "mapRendering": [ | ||||
|     { | ||||
|       "icon": { | ||||
|         "render": "circle:white;./assets/layers/doctors/doctors.svg", | ||||
|         "mappings": [ | ||||
|           { | ||||
|             "if": "amenity=dentist", | ||||
|             "then": "circle:white;./assets/layers/doctors/dentist.svg" | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "iconSize": "40,40,center", | ||||
|       "location": [ | ||||
|         "point", | ||||
|         "centroid" | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -41,5 +41,4 @@ | |||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| 
 | ||||
| } | ||||
|  | @ -3,9 +3,7 @@ | |||
|   "description": "Layer containing various presets and questions generated by ID. These are meant to be reused in other layers by importing the tagRenderings with `id_preset.<tagrendering>", | ||||
|   "#dont-translate": "*", | ||||
|   "source": { | ||||
|     "osmTags": { | ||||
|       "and": [] | ||||
|     } | ||||
|     "osmTags": "id~*" | ||||
|   }, | ||||
|   "mapRendering": null, | ||||
|   "tagRenderings": [ | ||||
|  |  | |||
|  | @ -1,68 +1,64 @@ | |||
| { | ||||
|     "id": "pharmacy", | ||||
|     "name": { | ||||
|         "en": "pharmacy" | ||||
|     }, | ||||
|     "title": { | ||||
|         "render": { | ||||
|             "en": "{name}" | ||||
|         } | ||||
|     }, | ||||
|     "source": { | ||||
|         "osmTags": { | ||||
|             "and": [ | ||||
|                 "amenity=pharmacy" | ||||
|             ] | ||||
|         } | ||||
|     }, | ||||
|     "minzoom":13, | ||||
|     "tagRenderings": [ | ||||
|         "images", | ||||
|         "opening_hours", | ||||
|         "phone", | ||||
|         "email", | ||||
|         "website", | ||||
|   "id": "pharmacy", | ||||
|   "name": { | ||||
|     "en": "pharmacy" | ||||
|   }, | ||||
|   "title": { | ||||
|     "render": { | ||||
|       "en": "{name}" | ||||
|     } | ||||
|   }, | ||||
|   "source": { | ||||
|     "osmTags": { | ||||
|       "and": [ | ||||
|         "amenity=pharmacy" | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   "minzoom": 13, | ||||
|   "tagRenderings": [ | ||||
|     "images", | ||||
|     "opening_hours", | ||||
|     "phone", | ||||
|     "email", | ||||
|     "website", | ||||
|     { | ||||
|       "id": "wheelchair", | ||||
|       "question": { | ||||
|         "en": "Is this pharmacy easy to access on a wheelchair?" | ||||
|       }, | ||||
|       "mappings": [ | ||||
|         { | ||||
|             "id": "wheelchair", | ||||
|             "render": { | ||||
|                 "en": "Easily accessible for wheelchair users: {wheelchair}" | ||||
|             }, | ||||
|             "question": { | ||||
|                 "en": "Is this pharmacy easy to access on a wheelchair?" | ||||
|             }, | ||||
|             "mappings": [ | ||||
|                 { | ||||
|                     "if": "wheelchair=yes", | ||||
|                     "then": { | ||||
|                         "en": "This pharmacy is easy to access on a wheelchair" | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     "if": "wheelchair=no", | ||||
|                     "then": { | ||||
|                         "en": "This pharmacy is hard to access on a wheelchair" | ||||
|                     } | ||||
|                      | ||||
|                 }, | ||||
|                 { | ||||
|                     "if": "wheelchair=limited", | ||||
|                     "then": { | ||||
|                         "en": "This pharmacy has limited access for wheelchair users" | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     ], | ||||
|     "mapRendering": [ | ||||
|           "if": "wheelchair=yes", | ||||
|           "then": { | ||||
|             "en": "This pharmacy is easy to access on a wheelchair" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|             "icon": { | ||||
|                 "render": "./assets/layers/pharmacy/pharmacy.svg" | ||||
|             }, | ||||
|             "iconSize": "40,40,bottom", | ||||
|             "location": [ | ||||
|                 "point", | ||||
|                 "centroid" | ||||
|             ] | ||||
|           "if": "wheelchair=no", | ||||
|           "then": { | ||||
|             "en": "This pharmacy is hard to access on a wheelchair" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "if": "wheelchair=limited", | ||||
|           "then": { | ||||
|             "en": "This pharmacy has limited access for wheelchair users" | ||||
|           } | ||||
|         } | ||||
|     ] | ||||
|       ] | ||||
|     } | ||||
|   ], | ||||
|   "mapRendering": [ | ||||
|     { | ||||
|       "icon": { | ||||
|         "render": "./assets/layers/pharmacy/pharmacy.svg" | ||||
|       }, | ||||
|       "iconSize": "40,40,bottom", | ||||
|       "location": [ | ||||
|         "point", | ||||
|         "centroid" | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -332,7 +332,7 @@ | |||
|             "title": "Sammenlign med eksisterende data", | ||||
|             "titleLive": "Live data på OSM", | ||||
|             "titleNearby": "Elementer i nærheden", | ||||
|             "zoomIn": "Live data bliver vist på zoomniveau mindst {needed}. Det aktuelle zoomniveau er {current}" | ||||
|             "zoomIn": "Det aktuelle zoomniveau er {current}" | ||||
|         }, | ||||
|         "createNotes": { | ||||
|             "creating": "Oprettede <b>{count}</b> noter ud af {total}", | ||||
|  |  | |||
|  | @ -360,7 +360,7 @@ | |||
|             "title": "Mit vorhandenen Daten vergleichen", | ||||
|             "titleLive": "Live-Daten auf OSM", | ||||
|             "titleNearby": "Objekte in der Nähe", | ||||
|             "zoomIn": "Live-Daten werden ab Zoomstufe {needed} angezeigt. Die aktuelle Zoomstufe ist {current}" | ||||
|             "zoomIn": "Die aktuelle Zoomstufe ist {current}" | ||||
|         }, | ||||
|         "createNotes": { | ||||
|             "creating": "<b>{count}</b> Notizen von {total} erstellt", | ||||
|  |  | |||
|  | @ -355,6 +355,7 @@ | |||
|             "osmLoaded": "{count} elements are loaded from OpenStreetMap which match the layer <b>{name}</b>.", | ||||
|             "reloadTheCache": "Clear the cache and query overpass again", | ||||
|             "setRangeToZero": "Set the range to 0 or 1 if you want to import them all", | ||||
|             "showOsmLayerInConflationMap": "Show the OSM data", | ||||
|             "states": { | ||||
|                 "error": "Could not load latest data from overpass due to {error}", | ||||
|                 "idle": "Checking local storage…", | ||||
|  | @ -364,7 +365,8 @@ | |||
|             "title": "Compare with existing data", | ||||
|             "titleLive": "Live data on OSM", | ||||
|             "titleNearby": "Nearby features", | ||||
|             "zoomIn": "The live data is shown if the zoomlevel is at least {needed}. The current zoom level is {current}" | ||||
|             "zoomIn": "The current zoom level is {current}", | ||||
|             "zoomLevelSelection": "The live data is shown if the zoomlevel is at least: " | ||||
|         }, | ||||
|         "createNotes": { | ||||
|             "creating": "Created <b>{count}</b> notes out of {total}", | ||||
|  |  | |||
|  | @ -309,7 +309,7 @@ | |||
|             "title": "Sammenlign med eksisterende data", | ||||
|             "titleLive": "Sanntidsdata på OSM", | ||||
|             "titleNearby": "Funksjoner i nærheten", | ||||
|             "zoomIn": "Sanntidsdata vises hvis forstørrelsesnivået er minst {needed}. Nåværende forstørrelsesnivå er {current}." | ||||
|             "zoomIn": "Nåværende forstørrelsesnivå er {current}." | ||||
|         }, | ||||
|         "createNotes": { | ||||
|             "creating": "Opprettet <b>{count}</b> notater av {total}", | ||||
|  |  | |||
|  | @ -360,7 +360,7 @@ | |||
|             "title": "Vergelijking met bestaande data", | ||||
|             "titleLive": "Data van OSM", | ||||
|             "titleNearby": "Objecten in de buurt", | ||||
|             "zoomIn": "De OSM-data wordt getoond vanaf zoomniveau {needed}. Het huidige zoomniveau is {current}" | ||||
|             "zoomIn": "Het huidige zoomniveau is {current}" | ||||
|         }, | ||||
|         "createNotes": { | ||||
|             "creating": "<b>{count}</b> van {total} kaartnota's werden gemaakt", | ||||
|  |  | |||
|  | @ -248,7 +248,7 @@ class TranslationPart { | |||
| 
 | ||||
|                     if (lang === "en" || usedByLanguage === "en") { | ||||
|                         errors.push({ | ||||
|                             error: `The translation for ${key} does not have the required subpart ${part}.
 | ||||
|                             error: `The translation for ${key} does not have the required subpart ${part} (in ${usedByLanguage}).
 | ||||
|     \tThe full translation is ${value} | ||||
|     \t${fixLink}`,
 | ||||
|                             path: path | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue