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) | ||||
|         for (const kv of this._basicTags) { | ||||
|             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; | ||||
|         } | ||||
|  |  | |||
|  | @ -19,6 +19,19 @@ import TitleHandler from "../Actors/TitleHandler"; | |||
| import {BBox} from "../BBox"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| 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 | ||||
|  | @ -82,7 +95,7 @@ export default class MapState extends UserRelatedState { | |||
|     /** | ||||
|      * 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 | ||||
|  | @ -127,9 +140,9 @@ export default class MapState extends UserRelatedState { | |||
|         this.overlayToggles = this.layoutToUse?.tileLayerSources | ||||
|             ?.filter(c => c.name !== undefined) | ||||
|             ?.map(c => ({ | ||||
|             config: c, | ||||
|             isDisplayed: QueryParameters.GetBooleanQueryParameter("overlay-" + c.id, c.defaultState, "Wether or not the overlay " + c.id + " is shown") | ||||
|         })) ?? [] | ||||
|                 config: c, | ||||
|                 isDisplayed: QueryParameters.GetBooleanQueryParameter("overlay-" + c.id, c.defaultState, "Wether or not the overlay " + c.id + " is shown") | ||||
|             })) ?? [] | ||||
|         this.filteredLayers = this.InitializeFilteredLayers() | ||||
| 
 | ||||
| 
 | ||||
|  | @ -212,7 +225,7 @@ export default class MapState extends UserRelatedState { | |||
|             return [feature] | ||||
|         }) | ||||
| 
 | ||||
|         this.currentView = new TiledStaticFeatureSource(features,  currentViewLayer); | ||||
|         this.currentView = new TiledStaticFeatureSource(features, currentViewLayer); | ||||
|     } | ||||
| 
 | ||||
|     private initGpsLocation() { | ||||
|  | @ -341,15 +354,15 @@ export default class MapState extends UserRelatedState { | |||
|     } | ||||
| 
 | ||||
|     private getPref(key: string, layer: LayerConfig): UIEventSource<boolean> { | ||||
|       const pref = this.osmConnection | ||||
|         const pref = this.osmConnection | ||||
|             .GetPreference(key) | ||||
|             .sync(v => { | ||||
|                 if(v === undefined){ | ||||
|                 if (v === undefined) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return v === "true"; | ||||
|             }, [], b => { | ||||
|                 if(b === undefined){ | ||||
|                 if (b === undefined) { | ||||
|                     return undefined | ||||
|                 } | ||||
|                 return "" + b; | ||||
|  | @ -360,7 +373,7 @@ export default class MapState extends UserRelatedState { | |||
| 
 | ||||
|     private InitializeFilteredLayers() { | ||||
|         const layoutToUse = this.layoutToUse; | ||||
|         if(layoutToUse === undefined){ | ||||
|         if (layoutToUse === undefined) { | ||||
|             return new UIEventSource<FilteredLayer[]>([]) | ||||
|         } | ||||
|         const flayers: FilteredLayer[] = []; | ||||
|  | @ -369,11 +382,11 @@ export default class MapState extends UserRelatedState { | |||
|             if (layer.syncSelection === "local") { | ||||
|                 isDisplayed = LocalStorageSource.GetParsed(layoutToUse.id + "-layer-" + layer.id + "-enabled", layer.shownByDefault) | ||||
|             } 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") { | ||||
|                 isDisplayed = this.getPref("layer-" + layer.id + "-enabled", layer) | ||||
|             } 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 + ") " | ||||
|     } | ||||
|      | ||||
|     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. | ||||
|      * Note that this method will never work perfectly | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import Toggle from "../Input/Toggle"; | |||
| import MapControlButton from "../MapControlButton"; | ||||
| import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"; | ||||
| import Svg from "../../Svg"; | ||||
| import MapState from "../../Logic/State/MapState"; | ||||
| import MapState, {GlobalFilter} from "../../Logic/State/MapState"; | ||||
| import LevelSelector from "../Input/LevelSelector"; | ||||
| import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; | ||||
| import {Utils} from "../../Utils"; | ||||
|  | @ -12,6 +12,9 @@ import {RegexTag} from "../../Logic/Tags/RegexTag"; | |||
| import {Or} from "../../Logic/Tags/Or"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
| 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 { | ||||
| 
 | ||||
|  | @ -50,10 +53,11 @@ export default class RightControls extends Combine { | |||
|             if (bbox === undefined) { | ||||
|                 return [] | ||||
|             } | ||||
|             const allElements = state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox); | ||||
|             const allLevelsRaw: string[] = [].concat(...allElements.map(allElements => allElements.features.map(f => <string>f.properties["level"]))) | ||||
|             const allElementsUnfiltered: OsmFeature[] = [].concat(... state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox).map(ff => ff.features)) | ||||
|             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)))  | ||||
|             if(allLevels.indexOf("0") < 0){ | ||||
|             if (allLevels.indexOf("0") < 0) { | ||||
|                 allLevels.push("0") | ||||
|             } | ||||
|             allLevels.sort((a, b) => a < b ? -1 : 1) | ||||
|  | @ -62,40 +66,57 @@ export default class RightControls extends Combine { | |||
|         state.globalFilters.data.push({ | ||||
|             filter: { | ||||
|                 currentFilter: undefined, | ||||
|                 state: undefined | ||||
|                 state: undefined, | ||||
| 
 | ||||
|             }, id: "level" | ||||
|             }, | ||||
|             id: "level", | ||||
|             onNewPoint: undefined | ||||
|         }) | ||||
|         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]) | ||||
| 
 | ||||
|         function setLevelFilter() { | ||||
|             const filter = state.globalFilters.data.find(gf => gf.id === "level") | ||||
|             const oldState = filter.filter.state; | ||||
|             console.log("Updating levels filter") | ||||
|             const filter: GlobalFilter = state.globalFilters.data.find(gf => gf.id === "level") | ||||
|             if (!isShown.data) { | ||||
|                 filter.filter = { | ||||
|                     state: "*", | ||||
|                     currentFilter: undefined | ||||
|                     currentFilter: undefined, | ||||
|                 } | ||||
|                 filter.onNewPoint = undefined | ||||
| 
 | ||||
|             } else { | ||||
| 
 | ||||
|                 const l = levelSelect.GetValue().data | ||||
|                 let neededLevel : TagsFilter =  new RegexTag("level", new RegExp("(^|;)" + l + "(;|$)")); | ||||
|                 if(l === "0"){ | ||||
|                 let neededLevel: TagsFilter = new RegexTag("level", new RegExp("(^|;)" + l + "(;|$)")); | ||||
|                 if (l === "0") { | ||||
|                     neededLevel = new Or([neededLevel, new Tag("level", "")]) | ||||
|                 } | ||||
|                 filter.filter = { | ||||
|                     state: l, | ||||
|                     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(); | ||||
|                 console.log("Level filter is now ", filter?.filter?.currentFilter?.asHumanString(false, false, {})) | ||||
|             } | ||||
|             state.globalFilters.ping(); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|  | @ -104,9 +125,9 @@ export default class RightControls extends Combine { | |||
|             console.log("Is level selector shown?", shown) | ||||
|             setLevelFilter() | ||||
|             if (shown) { | ||||
|                // levelSelect.SetClass("invisible")
 | ||||
|             } else { | ||||
|                 levelSelect.RemoveClass("invisible") | ||||
|             } else { | ||||
|                 levelSelect.SetClass("invisible") | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"; | |||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import Loading from "../Base/Loading"; | ||||
| import Hash from "../../Logic/Web/Hash"; | ||||
| import {GlobalFilter} from "../../Logic/State/MapState"; | ||||
| 
 | ||||
| /* | ||||
| * The SimpleAddUI is a single panel, which can have multiple states: | ||||
|  | @ -66,7 +67,8 @@ export default class SimpleAddUI extends Toggle { | |||
|                     locationControl: UIEventSource<Loc>, | ||||
|                     filteredLayers: UIEventSource<FilteredLayer[]>, | ||||
|                     featureSwitchFilter: UIEventSource<boolean>, | ||||
|                     backgroundLayer: UIEventSource<BaseLayer> | ||||
|                     backgroundLayer: UIEventSource<BaseLayer>, | ||||
|                     globalFilters: UIEventSource<GlobalFilter[]> | ||||
|                 },  | ||||
|                 takeLocationFrom?: UIEventSource<{lat: number, lon: number}> | ||||
|     ) { | ||||
|  |  | |||
|  | @ -15,12 +15,16 @@ import SimpleAddUI, {PresetInfo} from "../BigComponents/SimpleAddUI"; | |||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import Img from "../Base/Img"; | ||||
| 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 { | ||||
| 
 | ||||
| 
 | ||||
|     constructor( | ||||
|         state: { | ||||
|             globalFilters: UIEventSource<GlobalFilter[]>; | ||||
|             featureSwitchIsTesting: UIEventSource<boolean>; | ||||
|             osmConnection: OsmConnection, | ||||
|             featurePipeline: FeaturePipeline, | ||||
|  | @ -106,7 +110,11 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|         ).SetClass("font-bold break-words") | ||||
|             .onClick(() => { | ||||
|                 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) { | ||||
|  | @ -126,7 +134,7 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|                 .onClick(() => filterViewIsOpened.setData(true)) | ||||
| 
 | ||||
| 
 | ||||
|         const openLayerOrConfirm = new Toggle( | ||||
|         let openLayerOrConfirm = new Toggle( | ||||
|             confirmButton, | ||||
|             openLayerControl, | ||||
|             preset.layerToAddTo.isDisplayed | ||||
|  | @ -152,6 +160,29 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|             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 | ||||
|             .map(appliedFilters => { | ||||
|                 const activeFilters = Array.from(appliedFilters.values()).filter(f => f?.currentFilter !== undefined); | ||||
|  | @ -172,10 +203,10 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|         ).onClick(cancel) | ||||
| 
 | ||||
| 
 | ||||
|         let examples : BaseUIElement = undefined; | ||||
|         if(preset.exampleImages !== undefined && preset.exampleImages.length > 0){ | ||||
|         let examples: BaseUIElement = undefined; | ||||
|         if (preset.exampleImages !== undefined && preset.exampleImages.length > 0) { | ||||
|             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") | ||||
|             ]) | ||||
| 
 | ||||
|  |  | |||
|  | @ -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", | ||||
|             "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…", | ||||
|         "loadingTheme": "Loading {theme}…", | ||||
|         "loginFailed": "Logging in into OpenStreetMap failed", | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue