forked from MapComplete/MapComplete
		
	Add extra check that a feature is added on the right level; automatically add the right level to a new point
This commit is contained in:
		
							parent
							
								
									038b2ece4c
								
							
						
					
					
						commit
						effd75e95c
					
				
					 8 changed files with 140 additions and 46 deletions
				
			
		|  | @ -72,7 +72,7 @@ export default class CreateNewNodeAction extends OsmCreateAction { | ||||||
|         this.setElementId(id) |         this.setElementId(id) | ||||||
|         for (const kv of this._basicTags) { |         for (const kv of this._basicTags) { | ||||||
|             if (typeof kv.value !== "string") { |             if (typeof kv.value !== "string") { | ||||||
|                 throw "Invalid value: don't use a regex in a preset" |                 throw "Invalid value: don't use non-string value in a preset. The tag "+kv.key+"="+kv.value+" is not a string, the value is a "+typeof kv.value | ||||||
|             } |             } | ||||||
|             properties[kv.key] = kv.value; |             properties[kv.key] = kv.value; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -19,6 +19,19 @@ import TitleHandler from "../Actors/TitleHandler"; | ||||||
| import {BBox} from "../BBox"; | import {BBox} from "../BBox"; | ||||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||||
| import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource"; | import {TiledStaticFeatureSource} from "../FeatureSource/Sources/StaticFeatureSource"; | ||||||
|  | import {Translation, TypedTranslation} from "../../UI/i18n/Translation"; | ||||||
|  | import {Tag} from "../Tags/Tag"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export interface GlobalFilter { | ||||||
|  |     filter: FilterState, | ||||||
|  |     id: string, | ||||||
|  |     onNewPoint: { | ||||||
|  |         safetyCheck: Translation, | ||||||
|  |         confirmAddNew: TypedTranslation<{ preset: Translation }> | ||||||
|  |         tags: Tag[] | ||||||
|  |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Contains all the leaflet-map related state |  * Contains all the leaflet-map related state | ||||||
|  | @ -82,7 +95,7 @@ export default class MapState extends UserRelatedState { | ||||||
|     /** |     /** | ||||||
|      * Filters which apply onto all layers |      * Filters which apply onto all layers | ||||||
|      */ |      */ | ||||||
|     public globalFilters: UIEventSource<{ filter: FilterState, id: string }[]> = new UIEventSource([], "globalFilters") |     public globalFilters: UIEventSource<GlobalFilter[]> = new UIEventSource([], "globalFilters") | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Which overlays are shown |      * Which overlays are shown | ||||||
|  | @ -344,12 +357,12 @@ export default class MapState extends UserRelatedState { | ||||||
|         const pref = this.osmConnection |         const pref = this.osmConnection | ||||||
|             .GetPreference(key) |             .GetPreference(key) | ||||||
|             .sync(v => { |             .sync(v => { | ||||||
|                 if(v === undefined){ |                 if (v === undefined) { | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 return v === "true"; |                 return v === "true"; | ||||||
|             }, [], b => { |             }, [], b => { | ||||||
|                 if(b === undefined){ |                 if (b === undefined) { | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 return "" + b; |                 return "" + b; | ||||||
|  | @ -360,7 +373,7 @@ export default class MapState extends UserRelatedState { | ||||||
| 
 | 
 | ||||||
|     private InitializeFilteredLayers() { |     private InitializeFilteredLayers() { | ||||||
|         const layoutToUse = this.layoutToUse; |         const layoutToUse = this.layoutToUse; | ||||||
|         if(layoutToUse === undefined){ |         if (layoutToUse === undefined) { | ||||||
|             return new UIEventSource<FilteredLayer[]>([]) |             return new UIEventSource<FilteredLayer[]>([]) | ||||||
|         } |         } | ||||||
|         const flayers: FilteredLayer[] = []; |         const flayers: FilteredLayer[] = []; | ||||||
|  | @ -369,11 +382,11 @@ export default class MapState extends UserRelatedState { | ||||||
|             if (layer.syncSelection === "local") { |             if (layer.syncSelection === "local") { | ||||||
|                 isDisplayed = LocalStorageSource.GetParsed(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer.shownByDefault) |                 isDisplayed = LocalStorageSource.GetParsed(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer.shownByDefault) | ||||||
|             } else if (layer.syncSelection === "theme-only") { |             } else if (layer.syncSelection === "theme-only") { | ||||||
|                 isDisplayed = this.getPref(layoutToUse.id+ "-layer-" + layer.id + "-enabled", layer) |                 isDisplayed = this.getPref(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer) | ||||||
|             } else if (layer.syncSelection === "global") { |             } else if (layer.syncSelection === "global") { | ||||||
|                 isDisplayed = this.getPref("layer-" + layer.id + "-enabled", layer) |                 isDisplayed = this.getPref("layer-" + layer.id + "-enabled", layer) | ||||||
|             } else { |             } else { | ||||||
|                 isDisplayed = QueryParameters.GetBooleanQueryParameter("layer-" + layer.id, layer.shownByDefault, "Wether or not layer "+layer.id+" is shown") |                 isDisplayed = QueryParameters.GetBooleanQueryParameter("layer-" + layer.id, layer.shownByDefault, "Wether or not layer " + layer.id + " is shown") | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -493,6 +493,16 @@ export class TagUtils { | ||||||
|         return " (" + joined + ") " |         return " (" + joined + ") " | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     public static ExtractSimpleTags(tf: TagsFilter) : Tag[] { | ||||||
|  |         const result: Tag[] = [] | ||||||
|  |         tf.visit(t => { | ||||||
|  |             if(t instanceof Tag){ | ||||||
|  |                 result.push(t) | ||||||
|  |             } | ||||||
|  |         }) | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Returns 'true' is opposite tags are detected. |      * Returns 'true' is opposite tags are detected. | ||||||
|      * Note that this method will never work perfectly |      * Note that this method will never work perfectly | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import Toggle from "../Input/Toggle"; | ||||||
| import MapControlButton from "../MapControlButton"; | import MapControlButton from "../MapControlButton"; | ||||||
| import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"; | import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import MapState from "../../Logic/State/MapState"; | import MapState, {GlobalFilter} from "../../Logic/State/MapState"; | ||||||
| import LevelSelector from "../Input/LevelSelector"; | import LevelSelector from "../Input/LevelSelector"; | ||||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; | import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
|  | @ -12,6 +12,9 @@ import {RegexTag} from "../../Logic/Tags/RegexTag"; | ||||||
| import {Or} from "../../Logic/Tags/Or"; | import {Or} from "../../Logic/Tags/Or"; | ||||||
| import {Tag} from "../../Logic/Tags/Tag"; | import {Tag} from "../../Logic/Tags/Tag"; | ||||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||||
|  | import Translations from "../i18n/Translations"; | ||||||
|  | import {BBox} from "../../Logic/BBox"; | ||||||
|  | import {OsmFeature} from "../../Models/OsmFeature"; | ||||||
| 
 | 
 | ||||||
| export default class RightControls extends Combine { | export default class RightControls extends Combine { | ||||||
| 
 | 
 | ||||||
|  | @ -50,10 +53,11 @@ export default class RightControls extends Combine { | ||||||
|             if (bbox === undefined) { |             if (bbox === undefined) { | ||||||
|                 return [] |                 return [] | ||||||
|             } |             } | ||||||
|             const allElements = state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox); |             const allElementsUnfiltered: OsmFeature[] = [].concat(... state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox).map(ff => ff.features)) | ||||||
|             const allLevelsRaw: string[] = [].concat(...allElements.map(allElements => allElements.features.map(f => <string>f.properties["level"]))) |             const allElements = allElementsUnfiltered.filter(f => BBox.get(f).overlapsWith(bbox)) | ||||||
|  |             const allLevelsRaw: string[] = allElements.map(f => f.properties["level"]) | ||||||
|             const allLevels = [].concat(...allLevelsRaw.map(l => TagUtils.LevelsParser(l)))  |             const allLevels = [].concat(...allLevelsRaw.map(l => TagUtils.LevelsParser(l)))  | ||||||
|             if(allLevels.indexOf("0") < 0){ |             if (allLevels.indexOf("0") < 0) { | ||||||
|                 allLevels.push("0") |                 allLevels.push("0") | ||||||
|             } |             } | ||||||
|             allLevels.sort((a, b) => a < b ? -1 : 1) |             allLevels.sort((a, b) => a < b ? -1 : 1) | ||||||
|  | @ -62,40 +66,57 @@ export default class RightControls extends Combine { | ||||||
|         state.globalFilters.data.push({ |         state.globalFilters.data.push({ | ||||||
|             filter: { |             filter: { | ||||||
|                 currentFilter: undefined, |                 currentFilter: undefined, | ||||||
|                 state: undefined |                 state: undefined, | ||||||
| 
 | 
 | ||||||
|             }, id: "level" |             }, | ||||||
|  |             id: "level", | ||||||
|  |             onNewPoint: undefined | ||||||
|         }) |         }) | ||||||
|         const levelSelect = new LevelSelector(levelsInView) |         const levelSelect = new LevelSelector(levelsInView) | ||||||
| 
 | 
 | ||||||
|         const isShown = levelsInView.map(levelsInView => levelsInView.length !== 0 && state.locationControl.data.zoom >= 17, |         const isShown = levelsInView.map(levelsInView => { | ||||||
|  |                 if (levelsInView.length == 0) { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |                 if (state.locationControl.data.zoom <= 16) { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |                 if (levelsInView.length == 1 && levelsInView[0] == "0") { | ||||||
|  |                     return false | ||||||
|  |                 } | ||||||
|  |                 return true; | ||||||
|  |             }, | ||||||
|             [state.locationControl]) |             [state.locationControl]) | ||||||
| 
 | 
 | ||||||
|         function setLevelFilter() { |         function setLevelFilter() { | ||||||
|             const filter = state.globalFilters.data.find(gf => gf.id === "level") |             console.log("Updating levels filter") | ||||||
|             const oldState = filter.filter.state; |             const filter: GlobalFilter = state.globalFilters.data.find(gf => gf.id === "level") | ||||||
|             if (!isShown.data) { |             if (!isShown.data) { | ||||||
|                 filter.filter = { |                 filter.filter = { | ||||||
|                     state: "*", |                     state: "*", | ||||||
|                     currentFilter: undefined |                     currentFilter: undefined, | ||||||
|                 } |                 } | ||||||
|  |                 filter.onNewPoint = undefined | ||||||
| 
 | 
 | ||||||
|             } else { |             } else { | ||||||
| 
 | 
 | ||||||
|                 const l = levelSelect.GetValue().data |                 const l = levelSelect.GetValue().data | ||||||
|                 let neededLevel : TagsFilter =  new RegexTag("level", new RegExp("(^|;)" + l + "(;|$)")); |                 let neededLevel: TagsFilter = new RegexTag("level", new RegExp("(^|;)" + l + "(;|$)")); | ||||||
|                 if(l === "0"){ |                 if (l === "0") { | ||||||
|                     neededLevel = new Or([neededLevel, new Tag("level", "")]) |                     neededLevel = new Or([neededLevel, new Tag("level", "")]) | ||||||
|                 } |                 } | ||||||
|                 filter.filter = { |                 filter.filter = { | ||||||
|                     state: l, |                     state: l, | ||||||
|                     currentFilter: neededLevel |                     currentFilter: neededLevel | ||||||
|                 } |                 } | ||||||
|  |                 const t = Translations.t.general.levelSelection | ||||||
|  |                 filter.onNewPoint = { | ||||||
|  |                     confirmAddNew: t.confirmLevel.PartialSubs({level: l}), | ||||||
|  |                     safetyCheck: t.addNewOnLevel.Subs({level: l}), | ||||||
|  |                     tags: [new Tag("level", l)] | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|             if(filter.filter.state !== oldState){ |  | ||||||
|             state.globalFilters.ping(); |             state.globalFilters.ping(); | ||||||
|                 console.log("Level filter is now ", filter?.filter?.currentFilter?.asHumanString(false, false, {})) |  | ||||||
|             } |  | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -104,9 +125,9 @@ export default class RightControls extends Combine { | ||||||
|             console.log("Is level selector shown?", shown) |             console.log("Is level selector shown?", shown) | ||||||
|             setLevelFilter() |             setLevelFilter() | ||||||
|             if (shown) { |             if (shown) { | ||||||
|                // levelSelect.SetClass("invisible")
 |  | ||||||
|             } else { |  | ||||||
|                 levelSelect.RemoveClass("invisible") |                 levelSelect.RemoveClass("invisible") | ||||||
|  |             } else { | ||||||
|  |                 levelSelect.SetClass("invisible") | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"; | ||||||
| import BaseLayer from "../../Models/BaseLayer"; | import BaseLayer from "../../Models/BaseLayer"; | ||||||
| import Loading from "../Base/Loading"; | import Loading from "../Base/Loading"; | ||||||
| import Hash from "../../Logic/Web/Hash"; | import Hash from "../../Logic/Web/Hash"; | ||||||
|  | import {GlobalFilter} from "../../Logic/State/MapState"; | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
| * The SimpleAddUI is a single panel, which can have multiple states: | * The SimpleAddUI is a single panel, which can have multiple states: | ||||||
|  | @ -66,7 +67,8 @@ export default class SimpleAddUI extends Toggle { | ||||||
|                     locationControl: UIEventSource<Loc>, |                     locationControl: UIEventSource<Loc>, | ||||||
|                     filteredLayers: UIEventSource<FilteredLayer[]>, |                     filteredLayers: UIEventSource<FilteredLayer[]>, | ||||||
|                     featureSwitchFilter: UIEventSource<boolean>, |                     featureSwitchFilter: UIEventSource<boolean>, | ||||||
|                     backgroundLayer: UIEventSource<BaseLayer> |                     backgroundLayer: UIEventSource<BaseLayer>, | ||||||
|  |                     globalFilters: UIEventSource<GlobalFilter[]> | ||||||
|                 },  |                 },  | ||||||
|                 takeLocationFrom?: UIEventSource<{lat: number, lon: number}> |                 takeLocationFrom?: UIEventSource<{lat: number, lon: number}> | ||||||
|     ) { |     ) { | ||||||
|  |  | ||||||
|  | @ -15,12 +15,16 @@ import SimpleAddUI, {PresetInfo} from "../BigComponents/SimpleAddUI"; | ||||||
| import BaseLayer from "../../Models/BaseLayer"; | import BaseLayer from "../../Models/BaseLayer"; | ||||||
| import Img from "../Base/Img"; | import Img from "../Base/Img"; | ||||||
| import Title from "../Base/Title"; | import Title from "../Base/Title"; | ||||||
|  | import {GlobalFilter} from "../../Logic/State/MapState"; | ||||||
|  | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
|  | import {Tag} from "../../Logic/Tags/Tag"; | ||||||
| 
 | 
 | ||||||
| export default class ConfirmLocationOfPoint extends Combine { | export default class ConfirmLocationOfPoint extends Combine { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         state: { |         state: { | ||||||
|  |             globalFilters: UIEventSource<GlobalFilter[]>; | ||||||
|             featureSwitchIsTesting: UIEventSource<boolean>; |             featureSwitchIsTesting: UIEventSource<boolean>; | ||||||
|             osmConnection: OsmConnection, |             osmConnection: OsmConnection, | ||||||
|             featurePipeline: FeaturePipeline, |             featurePipeline: FeaturePipeline, | ||||||
|  | @ -106,7 +110,11 @@ export default class ConfirmLocationOfPoint extends Combine { | ||||||
|         ).SetClass("font-bold break-words") |         ).SetClass("font-bold break-words") | ||||||
|             .onClick(() => { |             .onClick(() => { | ||||||
|                 console.log("The confirmLocationPanel - precise input yielded ", preciseInput?.GetValue()?.data) |                 console.log("The confirmLocationPanel - precise input yielded ", preciseInput?.GetValue()?.data) | ||||||
|                 confirm(preset.tags, preciseInput?.GetValue()?.data ?? loc, preciseInput?.snappedOnto?.data?.properties?.id); |                 const globalFilterTagsToAdd: Tag[][] = state.globalFilters.data.filter(gf => gf.onNewPoint !== undefined) | ||||||
|  |                     .map(gf => gf.onNewPoint.tags) | ||||||
|  |                 const globalTags : Tag[] = [].concat(...globalFilterTagsToAdd) | ||||||
|  |                 console.log("Global tags to add are: ", globalTags) | ||||||
|  |                 confirm([...preset.tags, ...globalTags], preciseInput?.GetValue()?.data ?? loc, preciseInput?.snappedOnto?.data?.properties?.id); | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|         if (preciseInput !== undefined) { |         if (preciseInput !== undefined) { | ||||||
|  | @ -126,7 +134,7 @@ export default class ConfirmLocationOfPoint extends Combine { | ||||||
|                 .onClick(() => filterViewIsOpened.setData(true)) |                 .onClick(() => filterViewIsOpened.setData(true)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         const openLayerOrConfirm = new Toggle( |         let openLayerOrConfirm = new Toggle( | ||||||
|             confirmButton, |             confirmButton, | ||||||
|             openLayerControl, |             openLayerControl, | ||||||
|             preset.layerToAddTo.isDisplayed |             preset.layerToAddTo.isDisplayed | ||||||
|  | @ -152,6 +160,29 @@ export default class ConfirmLocationOfPoint extends Combine { | ||||||
|             closePopup() |             closePopup() | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  |         // We assume the number of global filters won't change during the run of the program
 | ||||||
|  |         for (let i = 0; i < state.globalFilters.data.length; i++) { | ||||||
|  |             const hasBeenCheckedOf = new UIEventSource(false); | ||||||
|  | 
 | ||||||
|  |             const filterConfirmPanel = new VariableUiElement( | ||||||
|  |                 state.globalFilters.map(gfs => { | ||||||
|  |                         const gf = gfs[i] | ||||||
|  |                         const confirm = gf.onNewPoint?.confirmAddNew?.Subs({preset: preset.title}) | ||||||
|  |                         return new Combine([ | ||||||
|  |                             gf.onNewPoint?.safetyCheck, | ||||||
|  |                             new SubtleButton(Svg.confirm_svg(), confirm).onClick(() => hasBeenCheckedOf.setData(true)) | ||||||
|  |                         ]) | ||||||
|  |                     } | ||||||
|  |                 )) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             openLayerOrConfirm = new Toggle( | ||||||
|  |                 openLayerOrConfirm, filterConfirmPanel, | ||||||
|  |                 state.globalFilters.map(f => hasBeenCheckedOf.data || f[i]?.onNewPoint === undefined, [hasBeenCheckedOf]) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         const hasActiveFilter = preset.layerToAddTo.appliedFilters |         const hasActiveFilter = preset.layerToAddTo.appliedFilters | ||||||
|             .map(appliedFilters => { |             .map(appliedFilters => { | ||||||
|                 const activeFilters = Array.from(appliedFilters.values()).filter(f => f?.currentFilter !== undefined); |                 const activeFilters = Array.from(appliedFilters.values()).filter(f => f?.currentFilter !== undefined); | ||||||
|  | @ -172,10 +203,10 @@ export default class ConfirmLocationOfPoint extends Combine { | ||||||
|         ).onClick(cancel) |         ).onClick(cancel) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         let examples : BaseUIElement = undefined; |         let examples: BaseUIElement = undefined; | ||||||
|         if(preset.exampleImages !== undefined && preset.exampleImages.length > 0){ |         if (preset.exampleImages !== undefined && preset.exampleImages.length > 0) { | ||||||
|             examples = new Combine([ |             examples = new Combine([ | ||||||
|              new Title( preset.exampleImages.length == 1 ?  Translations.t.general.example :  Translations.t.general.examples), |                 new Title(preset.exampleImages.length == 1 ? Translations.t.general.example : Translations.t.general.examples), | ||||||
|                 new Combine(preset.exampleImages.map(img => new Img(img).SetClass("h-64 m-1 w-auto rounded-lg"))).SetClass("flex flex-wrap items-stretch") |                 new Combine(preset.exampleImages.map(img => new Img(img).SetClass("h-64 m-1 w-auto rounded-lg"))).SetClass("flex flex-wrap items-stretch") | ||||||
|             ]) |             ]) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -319,4 +319,17 @@ export class TypedTranslation<T> extends Translation { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |     PartialSubs<X extends string>(text: Partial<T> & Record<X, string>): TypedTranslation<Omit<T, X>> { | ||||||
|  |         const newTranslations : Record<string, string> = {} | ||||||
|  |         for (const lang in this.translations) { | ||||||
|  |             const template = this.translations[lang] | ||||||
|  |             if(lang === "_context"){ | ||||||
|  |             newTranslations[lang] = template | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  |             newTranslations[lang] = Utils.SubstituteKeys(template, text, lang) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         return new TypedTranslation<Omit<T, X>>(newTranslations, this.context) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -140,6 +140,10 @@ | ||||||
|             "title": "Select layers", |             "title": "Select layers", | ||||||
|             "zoomInToSeeThisLayer": "Zoom in to see this layer" |             "zoomInToSeeThisLayer": "Zoom in to see this layer" | ||||||
|         }, |         }, | ||||||
|  |         "levelSelection": { | ||||||
|  |             "addNewOnLevel": "Is the new point location on level {level}?", | ||||||
|  |             "confirmLevel": "Yes, add {preset} on level {level}" | ||||||
|  |         }, | ||||||
|         "loading": "Loading…", |         "loading": "Loading…", | ||||||
|         "loadingTheme": "Loading {theme}…", |         "loadingTheme": "Loading {theme}…", | ||||||
|         "loginFailed": "Logging in into OpenStreetMap failed", |         "loginFailed": "Logging in into OpenStreetMap failed", | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue