From effd75e95c98940805b9303af77206b512e9def9 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Mon, 25 Jul 2022 16:55:44 +0200 Subject: [PATCH] Add extra check that a feature is added on the right level; automatically add the right level to a new point --- Logic/Osm/Actions/CreateNewNodeAction.ts | 2 +- Logic/State/MapState.ts | 37 +++++++++----- Logic/Tags/TagUtils.ts | 10 ++++ UI/BigComponents/RightControls.ts | 61 ++++++++++++++++-------- UI/BigComponents/SimpleAddUI.ts | 4 +- UI/NewPoint/ConfirmLocationOfPoint.ts | 51 ++++++++++++++++---- UI/i18n/Translation.ts | 17 ++++++- langs/en.json | 4 ++ 8 files changed, 140 insertions(+), 46 deletions(-) diff --git a/Logic/Osm/Actions/CreateNewNodeAction.ts b/Logic/Osm/Actions/CreateNewNodeAction.ts index 7a545c7e0..e6755ecd0 100644 --- a/Logic/Osm/Actions/CreateNewNodeAction.ts +++ b/Logic/Osm/Actions/CreateNewNodeAction.ts @@ -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; } diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index d5e5a1fa6..2e4a65a9c 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -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,8 +95,8 @@ 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 = 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 { - 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([]) } 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") } diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index 6ef3a8966..aa91fa9b5 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -492,6 +492,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. diff --git a/UI/BigComponents/RightControls.ts b/UI/BigComponents/RightControls.ts index 7063c5f7e..5edf81482 100644 --- a/UI/BigComponents/RightControls.ts +++ b/UI/BigComponents/RightControls.ts @@ -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 => f.properties["level"]))) - const allLevels = [].concat(...allLevelsRaw.map(l => TagUtils.LevelsParser(l))) - if(allLevels.indexOf("0") < 0){ + 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) { 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,12 +125,12 @@ 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") } }) - + levelSelect.GetValue().addCallback(_ => setLevelFilter()) super([new Combine([levelSelect]).SetClass(""), plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1"))) diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 4566f4115..4b19456a5 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -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, filteredLayers: UIEventSource, featureSwitchFilter: UIEventSource, - backgroundLayer: UIEventSource + backgroundLayer: UIEventSource, + globalFilters: UIEventSource }, takeLocationFrom?: UIEventSource<{lat: number, lon: number}> ) { diff --git a/UI/NewPoint/ConfirmLocationOfPoint.ts b/UI/NewPoint/ConfirmLocationOfPoint.ts index d2eb324f5..88671d310 100644 --- a/UI/NewPoint/ConfirmLocationOfPoint.ts +++ b/UI/NewPoint/ConfirmLocationOfPoint.ts @@ -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; featureSwitchIsTesting: UIEventSource; osmConnection: OsmConnection, featurePipeline: FeaturePipeline, @@ -38,8 +42,8 @@ export default class ConfirmLocationOfPoint extends Combine { let preciseInput: LocationInput = undefined if (preset.preciseInput !== undefined) { // Create location input - - + + // We uncouple the event source const zloc = {...loc, zoom: 19} const locationSrc = new UIEventSource(zloc); @@ -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); @@ -171,16 +202,16 @@ export default class ConfirmLocationOfPoint extends Combine { Translations.t.general.cancel ).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") ]) - + } - + super([ new Toggle( Translations.t.general.testing.SetClass("alert"), diff --git a/UI/i18n/Translation.ts b/UI/i18n/Translation.ts index e9988090a..eef10b59e 100644 --- a/UI/i18n/Translation.ts +++ b/UI/i18n/Translation.ts @@ -317,6 +317,19 @@ export class TypedTranslation extends Translation { return Utils.SubstituteKeys(template, text, lang); }, context) } - - + + + PartialSubs(text: Partial & Record): TypedTranslation> { + const newTranslations : Record = {} + 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>(newTranslations, this.context) + } } \ No newline at end of file diff --git a/langs/en.json b/langs/en.json index aa7323d5c..f359f52a9 100644 --- a/langs/en.json +++ b/langs/en.json @@ -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",