diff --git a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts index 1fbb7fbe0..dc97e1d3a 100644 --- a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts @@ -1,10 +1,9 @@ -import {UIEventSource} from "../../UIEventSource"; -import FilteredLayer from "../../../Models/FilteredLayer"; +import {Store, UIEventSource} from "../../UIEventSource"; +import FilteredLayer, {FilterState} from "../../../Models/FilteredLayer"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {BBox} from "../../BBox"; import {ElementStorage} from "../../ElementStorage"; import {TagsFilter} from "../../Tags/TagsFilter"; -import {tag} from "@turf/turf"; import {OsmFeature} from "../../../Models/OsmFeature"; export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled { @@ -16,7 +15,9 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti public readonly bbox: BBox private readonly upstream: FeatureSourceForLayer; private readonly state: { - locationControl: UIEventSource<{ zoom: number }>; selectedElement: UIEventSource, + locationControl: Store<{ zoom: number }>; + selectedElement: Store, + globalFilters: Store<{ filter: FilterState }[]>, allElements: ElementStorage }; private readonly _alreadyRegistered = new Set>(); @@ -25,9 +26,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti constructor( state: { - locationControl: UIEventSource<{ zoom: number }>, - selectedElement: UIEventSource, - allElements: ElementStorage + locationControl: Store<{ zoom: number }>, + selectedElement: Store, + allElements: ElementStorage, + globalFilters: Store<{ filter: FilterState }[]> }, tileIndex, upstream: FeatureSourceForLayer, @@ -60,6 +62,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti metataggingUpdated?.addCallback(_ => { self._is_dirty.setData(true) }) + + state.globalFilters.addCallback(_ => { + self.update() + }) this.update(); } @@ -69,6 +75,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti const layer = this.upstream.layer; const features: { feature: OsmFeature; freshness: Date }[] = (this.upstream.features.data ?? []); const includedFeatureIds = new Set(); + const globalFilters = self.state.globalFilters.data.map(f => f.filter); const newFeatures = (features ?? []).filter((f) => { self.registerCallback(f.feature) @@ -88,6 +95,14 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti } } + for (const filter of globalFilters) { + const neededTags: TagsFilter = filter?.currentFilter + if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) { + // Hidden by the filter on the layer itself - we want to hide it no matter what + return false; + } + } + includedFeatureIds.add(f.feature.properties.id) return true; }); diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index e1383afb7..ab665e6c3 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -8,6 +8,7 @@ import {FixedUiElement} from "../UI/Base/FixedUiElement"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import {CountryCoder} from "latlon2country" import Constants from "../Models/Constants"; +import {TagUtils} from "./Tags/TagUtils"; export class SimpleMetaTagger { @@ -32,7 +33,7 @@ export class SimpleMetaTagger { if (!docs.cleanupRetagger) { for (const key of docs.keys) { if (!key.startsWith('_') && key.toLowerCase().indexOf("theme") < 0) { - throw `Incorrect metakey ${key}: it should start with underscore (_)` + throw `Incorrect key for a calculated meta value '${key}': it should start with underscore (_)` } } } @@ -211,6 +212,27 @@ export default class SimpleMetaTaggers { return true; }) ); + private static levels = new SimpleMetaTagger( + { + doc: "Extract the 'level'-tag into a normalized, ';'-separated value", + keys: ["_level"] + }, + ((feature) => { + if (feature.properties["level"] === undefined) { + return false; + } + + const l = feature.properties["level"] + const newValue = TagUtils.LevelsParser(l).join(";") + if(l === newValue) { + return false; + } + feature.properties["level"] = newValue + return true + + }) + ) + private static canonicalize = new SimpleMetaTagger( { doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`)", @@ -218,7 +240,7 @@ export default class SimpleMetaTaggers { }, ((feature, _, __, state) => { - const units = Utils.NoNull([].concat(...state?.layoutToUse?.layers?.map(layer => layer.units )?? [])); + const units = Utils.NoNull([].concat(...state?.layoutToUse?.layers?.map(layer => layer.units) ?? [])); if (units.length == 0) { return; } @@ -317,7 +339,7 @@ export default class SimpleMetaTaggers { country_code: tags._country.toLowerCase(), state: undefined } - }, {tag_key: "opening_hours"}); + }, {tag_key: "opening_hours"}); // Recalculate! return oh.getState() ? "yes" : "no"; @@ -327,12 +349,12 @@ export default class SimpleMetaTaggers { delete tags._isOpen tags["_isOpen"] = "parse_error"; } - }}); - - + } + }); + + const tagsSource = state.allElements.getEventSourceById(feature.properties.id); - - + }) ) @@ -400,7 +422,8 @@ export default class SimpleMetaTaggers { SimpleMetaTaggers.currentTime, SimpleMetaTaggers.objectMetaInfo, SimpleMetaTaggers.noBothButLeftRight, - SimpleMetaTaggers.geometryType + SimpleMetaTaggers.geometryType, + SimpleMetaTaggers.levels ]; public static readonly lazyTags: string[] = [].concat(...SimpleMetaTaggers.metatags.filter(tagger => tagger.isLazy) diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index 2688d3140..6ef3a8966 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -127,7 +127,7 @@ export class TagUtils { * } * ]}) * TagUtils.FlattenMultiAnswer([tag]) // => TagUtils.Tag({and:["x=a;b", "y=0;1;2;3"] }) - * + * * TagUtils.FlattenMultiAnswer(([new Tag("x","y"), new Tag("a","b")])) // => new And([new Tag("x","y"), new Tag("a","b")]) * TagUtils.FlattenMultiAnswer(([new Tag("x","")])) // => new And([new Tag("x","")]) */ @@ -240,7 +240,7 @@ export class TagUtils { * * TagUtils.Tag("xyz<5").matchesProperties({xyz: 4}) // => true * TagUtils.Tag("xyz<5").matchesProperties({xyz: 5}) // => false - * + * * // RegexTags must match values with newlines * TagUtils.Tag("note~.*aed.*").matchesProperties({note: "Hier bevindt zich wss een defibrillator. \\n\\n De aed bevindt zich op de 5de verdieping"}) // => true * TagUtils.Tag("note~i~.*aed.*").matchesProperties({note: "Hier bevindt zich wss een defibrillator. \\n\\n De AED bevindt zich op de 5de verdieping"}) // => true @@ -264,13 +264,13 @@ export class TagUtils { * @constructor */ public static TagD(json?: TagConfigJson, context: string = ""): TagsFilter | undefined { - if(json === undefined){ + if (json === undefined) { return undefined } return TagUtils.Tag(json, context) } - - + + /** * INLINE sort of the given list */ @@ -581,4 +581,38 @@ export class TagUtils { return listToFilter.some(tf => guards.some(guard => guard.shadows(tf))) } + + /** + * Parses a level specifier to the various available levels + * + * TagUtils.LevelsParser("0") // => ["0"] + * TagUtils.LevelsParser("1") // => ["1"] + * TagUtils.LevelsParser("0;2") // => ["0","2"] + * TagUtils.LevelsParser("0-5") // => ["0","1","2","3","4","5"] + * TagUtils.LevelsParser("0") // => ["0"] + * TagUtils.LevelsParser("-1") // => ["-1"] + * TagUtils.LevelsParser("0;-1") // => ["0", "-1"] + */ + public static LevelsParser(level: string): string[] { + let spec = Utils.NoNull([level]) + spec = [].concat(...spec.map(s => s?.split(";"))) + spec = [].concat(...spec.map(s => { + s = s.trim() + if (s.indexOf("-") < 0 || s.startsWith("-")) { + return s + } + const [start, end] = s.split("-").map(s => Number(s.trim())) + if (isNaN(start) || isNaN(end)) { + return undefined + } + const values = [] + for (let i = start; i <= end; i++) { + values.push(i + "") + } + return values + })) + return Utils.NoNull(spec); + } + + } \ No newline at end of file diff --git a/UI/BigComponents/RightControls.ts b/UI/BigComponents/RightControls.ts index 3de47a3ee..7063c5f7e 100644 --- a/UI/BigComponents/RightControls.ts +++ b/UI/BigComponents/RightControls.ts @@ -7,6 +7,11 @@ import MapState from "../../Logic/State/MapState"; import LevelSelector from "../Input/LevelSelector"; import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; import {Utils} from "../../Utils"; +import {TagUtils} from "../../Logic/Tags/TagUtils"; +import {RegexTag} from "../../Logic/Tags/RegexTag"; +import {Or} from "../../Logic/Tags/Or"; +import {Tag} from "../../Logic/Tags/Tag"; +import {TagsFilter} from "../../Logic/Tags/TagsFilter"; export default class RightControls extends Combine { @@ -42,26 +47,72 @@ export default class RightControls extends Combine { }); const levelsInView = state.currentBounds.map(bbox => { - if(bbox === undefined){ + 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 => LevelSelector.LevelsParser(l))) - allLevels.sort((a,b) => a < b ? -1 : 1) + 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) return Utils.Dedup(allLevels) }) + state.globalFilters.data.push({ + filter: { + currentFilter: undefined, + state: undefined + + }, id: "level" + }) const levelSelect = new LevelSelector(levelsInView) - - levelsInView.addCallbackAndRun(levelsInView => { - if(levelsInView.length <= 1){ - levelSelect.SetClass("invisible") - }else{ + + const isShown = levelsInView.map(levelsInView => levelsInView.length !== 0 && state.locationControl.data.zoom >= 17, + [state.locationControl]) + + function setLevelFilter() { + const filter = state.globalFilters.data.find(gf => gf.id === "level") + const oldState = filter.filter.state; + if (!isShown.data) { + filter.filter = { + state: "*", + currentFilter: undefined + } + + } else { + + const l = levelSelect.GetValue().data + 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 + } + } + if(filter.filter.state !== oldState){ + state.globalFilters.ping(); + console.log("Level filter is now ", filter?.filter?.currentFilter?.asHumanString(false, false, {})) + } + return; + } + + + isShown.addCallbackAndRun(shown => { + console.log("Is level selector shown?", shown) + setLevelFilter() + if (shown) { + // levelSelect.SetClass("invisible") + } else { levelSelect.RemoveClass("invisible") } }) + + levelSelect.GetValue().addCallback(_ => setLevelFilter()) - super([levelSelect, plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1"))) + super([new Combine([levelSelect]).SetClass(""), plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1"))) this.SetClass("flex flex-col items-center") } diff --git a/UI/Input/LevelSelector.ts b/UI/Input/LevelSelector.ts index 2c5e6a4cf..766a1f6c8 100644 --- a/UI/Input/LevelSelector.ts +++ b/UI/Input/LevelSelector.ts @@ -4,7 +4,6 @@ import Combine from "../Base/Combine"; import Slider from "./Slider"; import {ClickableToggle} from "./Toggle"; import {FixedUiElement} from "../Base/FixedUiElement"; -import {Utils} from "../../Utils"; import {VariableUiElement} from "../Base/VariableUIElement"; export default class LevelSelector extends VariableUiElement implements InputElement { @@ -16,11 +15,15 @@ export default class LevelSelector extends VariableUiElement implements InputEle }) { const value = options?.value ?? new UIEventSource(undefined) super(Stores.ListStabilized(currentLevels).map(levels => { + console.log("CUrrent levels are", levels) let slider = new Slider(0, levels.length - 1, {vertical: true}); - slider.SetClass("flex m-1 elevatorslider mb-0 mt-8").SetStyle("height: " + 2.5 * levels.length + "rem ") - const toggleClass = "flex border-2 border-blue-500 w-10 h-10 place-content-center items-center" + const toggleClass = "flex border-2 border-blue-500 w-10 h-10 place-content-center items-center border-box" + slider.SetClass("flex elevator w-10").SetStyle(`height: ${2.5 * levels.length}rem; background: #00000000`) + const values = levels.map((data, i) => new ClickableToggle( - new FixedUiElement(data).SetClass("active bg-subtle " + toggleClass), new FixedUiElement(data).SetClass(toggleClass), slider.GetValue().sync( + new FixedUiElement(data).SetClass("font-bold active bg-subtle " + toggleClass), + new FixedUiElement(data).SetClass("normal-background " + toggleClass), + slider.GetValue().sync( (sliderVal) => { return sliderVal === i }, @@ -30,11 +33,13 @@ export default class LevelSelector extends VariableUiElement implements InputEle } )) .ToggleOnClick() - .SetClass("flex flex-column ml-5 bg-slate-200 w-10 h-10 valuesContainer")) + .SetClass("flex w-10 h-10")) - const combine = new Combine([new Combine(values).SetClass("mt-8"), slider]) - combine.SetClass("flex flex-row h-14"); + values.reverse(/* This is a new list, no side-effects */) + const combine = new Combine([new Combine(values), slider]) + combine.SetClass("flex flex-row overflow-hidden"); + slider.GetValue().addCallbackAndRun(i => { if (currentLevels?.data === undefined) { return @@ -47,6 +52,8 @@ export default class LevelSelector extends VariableUiElement implements InputEle }) return combine })) + + this._value = value } @@ -59,35 +66,5 @@ export default class LevelSelector extends VariableUiElement implements InputEle } - /** - * Parses a level specifier to the various available levels - * - * LevelSelector.LevelsParser("0") // => ["0"] - * LevelSelector.LevelsParser("1") // => ["1"] - * LevelSelector.LevelsParser("0;2") // => ["0","2"] - * LevelSelector.LevelsParser("0-5") // => ["0","1","2","3","4","5"] - * LevelSelector.LevelsParser("0") // => ["0"] - */ - public static LevelsParser(level: string): string[] { - let spec = [level] - spec = [].concat(...spec.map(s => s.split(";"))) - spec = [].concat(...spec.map(s => { - s = s.trim() - if (s.indexOf("-") < 0) { - return s - } - const [start, end] = s.split("-").map(s => Number(s.trim())) - if (isNaN(start) || isNaN(end)) { - return undefined - } - const values = [] - for (let i = start; i <= end; i++) { - values.push(i + "") - } - return values - })) - return Utils.NoNull(spec); - } - } \ No newline at end of file diff --git a/assets/svg/elevator_wheelchair.svg b/assets/svg/elevator_wheelchair.svg index 35b934aee..568caa468 100644 --- a/assets/svg/elevator_wheelchair.svg +++ b/assets/svg/elevator_wheelchair.svg @@ -1 +1,76 @@ - \ No newline at end of file + + + + + + + + + + + + + + diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 65bca52fe..d242eee6c 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -946,18 +946,6 @@ video { margin-right: 0px; } -.mb-0 { - margin-bottom: 0px; -} - -.mt-8 { - margin-top: 2rem; -} - -.ml-5 { - margin-left: 1.25rem; -} - .mr-3 { margin-right: 0.75rem; } @@ -974,6 +962,10 @@ video { margin-right: 0.25rem; } +.mb-0 { + margin-bottom: 0px; +} + .box-border { box-sizing: border-box; } @@ -1106,10 +1098,6 @@ video { height: 4rem; } -.h-14 { - height: 3.5rem; -} - .h-0 { height: 0px; } @@ -1527,6 +1515,11 @@ video { background-color: rgba(224, 231, 255, var(--tw-bg-opacity)); } +.bg-red-500 { + --tw-bg-opacity: 1; + background-color: rgba(239, 68, 68, var(--tw-bg-opacity)); +} + .bg-black { --tw-bg-opacity: 1; background-color: rgba(0, 0, 0, var(--tw-bg-opacity)); @@ -1547,11 +1540,6 @@ video { background-color: rgba(209, 213, 219, var(--tw-bg-opacity)); } -.bg-red-500 { - --tw-bg-opacity: 1; - background-color: rgba(239, 68, 68, var(--tw-bg-opacity)); -} - .bg-red-200 { --tw-bg-opacity: 1; background-color: rgba(254, 202, 202, var(--tw-bg-opacity)); @@ -2072,29 +2060,17 @@ input[type="range"].vertical { /* IE */ -webkit-appearance: slider-vertical; /* Chromium */ - width: 8px; - height: 180px; - padding: 31px 5px 0 5px; cursor: pointer; } @-moz-document url-prefix() { - input[type="range"].vertical { - height: 269px !important; - width: 65px !important; - } - - .valuesContainer { - padding-top: 30px; - } - - input[type="range"].vertical::-moz-range-thumb { - width: 150px; - height: 30px; - border: 2px; - border-style: solid; + input[type="range"].elevator::-moz-range-thumb { background-color: #00000000 !important; background-image: url("/assets/svg/elevator_wheelchair.svg"); + width: 150px !important; + height: 30px !important; + border: 2px; + border-style: solid; background-size: contain; background-position: center center; background-repeat: no-repeat; diff --git a/index.css b/index.css index 35d55c462..1db74d868 100644 --- a/index.css +++ b/index.css @@ -232,27 +232,17 @@ a { input[type="range"].vertical { writing-mode: bt-lr; /* IE */ -webkit-appearance: slider-vertical; /* Chromium */ - width: 8px; - height: 180px; - padding: 31px 5px 0 5px; cursor: pointer; } @-moz-document url-prefix() { - input[type="range"].vertical { - height: 269px !important; - width: 65px !important; - } - .valuesContainer { - padding-top: 30px; - } - input[type="range"].vertical::-moz-range-thumb { - width: 150px; - height: 30px; - border: 2px; - border-style: solid; + input[type="range"].elevator::-moz-range-thumb { background-color: #00000000 !important; background-image: url("/assets/svg/elevator_wheelchair.svg"); + width: 150px !important; + height: 30px !important; + border: 2px; + border-style: solid; background-size: contain; background-position: center center; background-repeat: no-repeat; diff --git a/test.ts b/test.ts index e69de29bb..f6a656f45 100644 --- a/test.ts +++ b/test.ts @@ -0,0 +1,4 @@ +import LevelSelector from "./UI/Input/LevelSelector"; +import {UIEventSource} from "./Logic/UIEventSource"; + +new LevelSelector(new UIEventSource(["0","1","2","2.5","x","3"])).AttachTo("maindiv") \ No newline at end of file