diff --git a/Logic/FeatureSource/Sources/StaticFeatureSource.ts b/Logic/FeatureSource/Sources/StaticFeatureSource.ts index fac0243a9..3a1b3baca 100644 --- a/Logic/FeatureSource/Sources/StaticFeatureSource.ts +++ b/Logic/FeatureSource/Sources/StaticFeatureSource.ts @@ -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, 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, diff --git a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts index c7cdae0a4..330c0386f 100644 --- a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts @@ -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 = > OsmToGeoJson.default(osmJson, // @ts-ignore { flatProperties: true diff --git a/Logic/Osm/Actions/ReplaceGeometryAction.ts b/Logic/Osm/Actions/ReplaceGeometryAction.ts index 0e0fa84e7..4e125f2a5 100644 --- a/Logic/Osm/Actions/ReplaceGeometryAction.ts +++ b/Logic/Osm/Actions/ReplaceGeometryAction.ts @@ -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 { 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", diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts index 889d6bae2..d4a5621a0 100644 --- a/Logic/Osm/Overpass.ts +++ b/Logic/Osm/Overpass.ts @@ -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); diff --git a/Logic/Web/IdbLocalStorage.ts b/Logic/Web/IdbLocalStorage.ts index 78930e011..f3d929866 100644 --- a/Logic/Web/IdbLocalStorage.ts +++ b/Logic/Web/IdbLocalStorage.ts @@ -7,8 +7,12 @@ import {Utils} from "../../Utils"; */ export class IdbLocalStorage { - + private static readonly _sourceCache: Record> = {} + public static Get(key: string, options?: { defaultValue?: T, whenLoaded?: (t: T | null) => void }): UIEventSource { + if(IdbLocalStorage._sourceCache[key] !== undefined){ + return IdbLocalStorage._sourceCache[key] + } const src = new UIEventSource(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; } diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index 36da25536..ea49777a9 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -75,8 +75,8 @@ export class DoesImageExist extends DesugaringStep { 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)) { diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts index 68d3c4944..2e15e95e2 100644 --- a/UI/Base/ScrollableFullScreen.ts +++ b/UI/Base/ScrollableFullScreen.ts @@ -28,7 +28,10 @@ export default class ScrollableFullScreen extends UIElement { constructor(title: ((options: { mode: string }) => BaseUIElement), content: ((options: { mode: string, resetScrollSignal: UIEventSource }) => BaseUIElement), hashToShow: string, - isShown: UIEventSource = new UIEventSource(false) + isShown: UIEventSource = new UIEventSource(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... diff --git a/UI/BigComponents/BackgroundMapSwitch.ts b/UI/BigComponents/BackgroundMapSwitch.ts index 68e3e77d9..b249f5abb 100644 --- a/UI/BigComponents/BackgroundMapSwitch.ts +++ b/UI/BigComponents/BackgroundMapSwitch.ts @@ -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, diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index 5255f5a59..30af72531 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -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") diff --git a/UI/ImportFlow/ConflationChecker.ts b/UI/ImportFlow/ConflationChecker.ts index 250da0ea9..88c394bf8 100644 --- a/UI/ImportFlow/ConflationChecker.ts +++ b/UI/ImportFlow/ConflationChecker.ts @@ -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(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 = fromLocalStorage.map(d => { + const geojson: Store = fromLocalStorage.map(d => { if (d === undefined) { return undefined } return d[0] }) - + const background = new UIEventSource(AvailableBaseLayers.osmCarto) const location = new UIEventSource({lat: 0, lon: 0, zoom: 1}) const currentBounds = new UIEventSource(undefined) - const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement() + const zoomLevel = ValidatedTextField.ForType("pnat").ConstructInputElement({ + value: LocalStorageSource.GetParsed("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 = 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([{ + layerDef: layer, + isDisplayed: new UIEventSource(true), + appliedFilters: new UIEventSource>(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 = 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({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) } diff --git a/UI/ImportFlow/ImportUtils.ts b/UI/ImportFlow/ImportUtils.ts index 5ce200fff..850cbacfc 100644 --- a/UI/ImportFlow/ImportUtils.ts +++ b/UI/ImportFlow/ImportUtils.ts @@ -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): Store<{ hasNearby: any[], noNearby: any[] }> { + public static partitionFeaturesIfNearby( + toPartitionFeatureCollection: ({ features: Feature[] }), + compareWith: Store<{ features: Feature[] }>, + cutoffDistanceInMeters: Store) + : 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( toImportElement.geometry.coordinates, GeoOperations.centerpointCoordinates(f))) if (hasNearbyFeature) { hasNearby.push(toImportElement) } else { diff --git a/UI/ImportFlow/MapPreview.ts b/UI/ImportFlow/MapPreview.ts index f4e8d666e..cb1a1049f 100644 --- a/UI/ImportFlow/MapPreview.ts +++ b/UI/ImportFlow/MapPreview.ts @@ -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 ]); diff --git a/UI/Input/Checkboxes.ts b/UI/Input/Checkboxes.ts index 3b0e1da96..236e7b831 100644 --- a/UI/Input/Checkboxes.ts +++ b/UI/Input/Checkboxes.ts @@ -4,13 +4,13 @@ import {Utils} from "../../Utils"; import BaseUIElement from "../BaseUIElement"; import InputElementMap from "./InputElementMap"; -export class CheckBox extends InputElementMap { +export class CheckBox extends InputElementMap { 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) diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 52fa5c664..143fb6e5d 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -25,16 +25,20 @@ export default class FeatureInfoBox extends ScrollableFullScreen { tags: UIEventSource, layerConfig: LayerConfig, state: FeaturePipelineState, - hashToShow?: string, - isShown?: UIEventSource, + options?: { + hashToShow?: string, + isShown?: UIEventSource, + 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"; diff --git a/UI/ShowDataLayer/ShowDataLayerImplementation.ts b/UI/ShowDataLayer/ShowDataLayerImplementation.ts index 4a2df70d8..34e45646e 100644 --- a/UI/ShowDataLayer/ShowDataLayerImplementation.ts +++ b/UI/ShowDataLayer/ShowDataLayerImplementation.ts @@ -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; + private readonly _leafletMap: Store; private readonly _enablePopups: boolean; private readonly _features: RenderingMultiPlexerFeatureSource private readonly _layerToShow: LayerConfig; diff --git a/UI/ShowDataLayer/ShowDataLayerOptions.ts b/UI/ShowDataLayer/ShowDataLayerOptions.ts index cd0dfd7f4..036faaf43 100644 --- a/UI/ShowDataLayer/ShowDataLayerOptions.ts +++ b/UI/ShowDataLayer/ShowDataLayerOptions.ts @@ -7,7 +7,7 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen"; export interface ShowDataLayerOptions { features: FeatureSource, selectedElement?: UIEventSource, - leafletMap: UIEventSource, + leafletMap: Store, popup?: undefined | ((tags: UIEventSource, layer: LayerConfig) => ScrollableFullScreen), zoomToFeatures?: false | boolean, doShowLayer?: Store, diff --git a/UI/ShowDataLayer/ShowDataMultiLayer.ts b/UI/ShowDataLayer/ShowDataMultiLayer.ts index 374539b60..4e20d1020 100644 --- a/UI/ShowDataLayer/ShowDataMultiLayer.ts +++ b/UI/ShowDataLayer/ShowDataMultiLayer.ts @@ -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 }) { + constructor(options: ShowDataLayerOptions & { layers: Store }) { new PerLayerFeatureSourceSplitter(options.layers, (perLayer => { const newOptions = { diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index 4a9b37dc3..a2ec63764 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -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; diff --git a/Utils.ts b/Utils.ts index 68a485c80..0da5a205e 100644 --- a/Utils.ts +++ b/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) { diff --git a/assets/layers/doctors/doctors.json b/assets/layers/doctors/doctors.json index cb9b0a65c..1d883134c 100644 --- a/assets/layers/doctors/doctors.json +++ b/assets/layers/doctors/doctors.json @@ -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" + ] + } + ] } \ No newline at end of file diff --git a/assets/layers/hospital/hospital.json b/assets/layers/hospital/hospital.json index e6337fd17..b773e9992 100644 --- a/assets/layers/hospital/hospital.json +++ b/assets/layers/hospital/hospital.json @@ -41,5 +41,4 @@ ] } ] -} - +} \ No newline at end of file diff --git a/assets/layers/id_presets/id_presets.json b/assets/layers/id_presets/id_presets.json index 9d170b584..1c30d295e 100644 --- a/assets/layers/id_presets/id_presets.json +++ b/assets/layers/id_presets/id_presets.json @@ -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.", "#dont-translate": "*", "source": { - "osmTags": { - "and": [] - } + "osmTags": "id~*" }, "mapRendering": null, "tagRenderings": [ diff --git a/assets/layers/pharmacy/pharmacy.json b/assets/layers/pharmacy/pharmacy.json index 3c8c946b7..2d982829a 100644 --- a/assets/layers/pharmacy/pharmacy.json +++ b/assets/layers/pharmacy/pharmacy.json @@ -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" + ] + } + ] } \ No newline at end of file diff --git a/langs/da.json b/langs/da.json index 0f2e9ce1e..3f7f21761 100644 --- a/langs/da.json +++ b/langs/da.json @@ -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 {count} noter ud af {total}", diff --git a/langs/de.json b/langs/de.json index f5ea64aaa..c2364515a 100644 --- a/langs/de.json +++ b/langs/de.json @@ -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": "{count} Notizen von {total} erstellt", diff --git a/langs/en.json b/langs/en.json index e0109cc12..45b119476 100644 --- a/langs/en.json +++ b/langs/en.json @@ -355,6 +355,7 @@ "osmLoaded": "{count} elements are loaded from OpenStreetMap which match the layer {name}.", "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 {count} notes out of {total}", diff --git a/langs/nb_NO.json b/langs/nb_NO.json index 6fe174c8c..329a1076d 100644 --- a/langs/nb_NO.json +++ b/langs/nb_NO.json @@ -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 {count} notater av {total}", diff --git a/langs/nl.json b/langs/nl.json index 51f855be3..0e4fd61e7 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -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": "{count} van {total} kaartnota's werden gemaakt", diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index ccbcdb7b6..cc0387e7b 100644 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -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