diff --git a/Logic/Tags/TagUtils.ts b/Logic/Tags/TagUtils.ts index aa91fa9b5..c9c9836e7 100644 --- a/Logic/Tags/TagUtils.ts +++ b/Logic/Tags/TagUtils.ts @@ -602,6 +602,7 @@ export class TagUtils { * TagUtils.LevelsParser("0") // => ["0"] * TagUtils.LevelsParser("-1") // => ["-1"] * TagUtils.LevelsParser("0;-1") // => ["0", "-1"] + * TagUtils.LevelsParser(undefined) // => [] */ public static LevelsParser(level: string): string[] { let spec = Utils.NoNull([level]) diff --git a/UI/BigComponents/LevelSelector.ts b/UI/BigComponents/LevelSelector.ts new file mode 100644 index 000000000..fa84fd4b4 --- /dev/null +++ b/UI/BigComponents/LevelSelector.ts @@ -0,0 +1,140 @@ +import FloorLevelInputElement from "../Input/FloorLevelInputElement"; +import MapState, {GlobalFilter} from "../../Logic/State/MapState"; +import {TagsFilter} from "../../Logic/Tags/TagsFilter"; +import {RegexTag} from "../../Logic/Tags/RegexTag"; +import {Or} from "../../Logic/Tags/Or"; +import {Tag} from "../../Logic/Tags/Tag"; +import Translations from "../i18n/Translations"; +import Combine from "../Base/Combine"; +import {OsmFeature} from "../../Models/OsmFeature"; +import {BBox} from "../../Logic/BBox"; +import {TagUtils} from "../../Logic/Tags/TagUtils"; +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; +import {Store} from "../../Logic/UIEventSource"; + +/*** + * The element responsible for the level input element and picking the right level, showing and hiding at the right time, ... + */ +export default class LevelSelector extends Combine { + + constructor(state: MapState & { featurePipeline: FeaturePipeline }) { + + const levelsInView : Store< Record> = state.currentBounds.map(bbox => { + if (bbox === undefined) { + return {} + } + 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 levels : Record = {"0": 0} + for (const levelDescription of allLevelsRaw) { + if(levelDescription === undefined){ + levels["0"] ++ + } + for (const level of TagUtils.LevelsParser(levelDescription)) { + levels[level] = (levels[level] ?? 0) + 1 + } + } + + return levels + }) + + const levelSelect = new FloorLevelInputElement(levelsInView) + + state.globalFilters.data.push({ + filter: { + currentFilter: undefined, + state: undefined, + + }, + id: "level", + onNewPoint: undefined + }) + const isShown = levelsInView.map(levelsInView => { + if (state.locationControl.data.zoom <= 16) { + return false; + } + if (Object.keys(levelsInView).length == 1) { + return false; + } + + return true; + }, + [state.locationControl]) + + function setLevelFilter() { + console.log("Updating levels filter to ", levelSelect.GetValue().data, " is shown:", isShown.data) + const filter: GlobalFilter = state.globalFilters.data.find(gf => gf.id === "level") + if (!isShown.data) { + filter.filter = { + state: "*", + currentFilter: undefined, + } + filter.onNewPoint = undefined + state.globalFilters.ping(); + return + } + + const l = levelSelect.GetValue().data + if(l === undefined){ + return + } + + 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)] + } + state.globalFilters.ping(); + return; + } + + + isShown.addCallbackAndRun(shown => { + console.log("Is level selector shown?", shown) + setLevelFilter() + if (shown) { + levelSelect.RemoveClass("invisible") + } else { + levelSelect.SetClass("invisible") + } + }) + + + levelsInView.addCallbackAndRun(levels => { + if(!isShown.data){ + return + } + const value = levelSelect.GetValue() + if (!(levels[value.data] === undefined || levels[value.data] === 0)) { + return; + } + // Nothing in view. Lets switch to a different level (the level with the most features) + let mostElements = 0 + let mostElementsLevel = undefined + for (const level in levels) { + const count = levels[level] + if(mostElementsLevel === undefined || mostElements < count){ + mostElementsLevel = level + mostElements = count + } + } + console.log("Force switching to a different level:", mostElementsLevel,"as it has",mostElements,"elements on that floor",levels,"(old level: "+value.data+")") + value.setData(mostElementsLevel ) + + }) + levelSelect.GetValue().addCallback(_ => setLevelFilter()) + super([levelSelect]) + } + +} \ No newline at end of file diff --git a/UI/BigComponents/RightControls.ts b/UI/BigComponents/RightControls.ts index 5edf81482..ac95b9a21 100644 --- a/UI/BigComponents/RightControls.ts +++ b/UI/BigComponents/RightControls.ts @@ -3,18 +3,13 @@ import Toggle from "../Input/Toggle"; import MapControlButton from "../MapControlButton"; import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"; import Svg from "../../Svg"; -import MapState, {GlobalFilter} from "../../Logic/State/MapState"; -import LevelSelector from "../Input/LevelSelector"; +import MapState from "../../Logic/State/MapState"; 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"; -import Translations from "../i18n/Translations"; import {BBox} from "../../Logic/BBox"; import {OsmFeature} from "../../Models/OsmFeature"; +import LevelSelector from "./LevelSelector"; export default class RightControls extends Combine { @@ -49,91 +44,8 @@ export default class RightControls extends Combine { state.locationControl.ping(); }); - const levelsInView = state.currentBounds.map(bbox => { - if (bbox === undefined) { - return [] - } - 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) - return Utils.Dedup(allLevels) - }) - state.globalFilters.data.push({ - filter: { - currentFilter: undefined, - state: undefined, - - }, - id: "level", - onNewPoint: undefined - }) - const levelSelect = new LevelSelector(levelsInView) - - 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() { - console.log("Updating levels filter") - const filter: GlobalFilter = state.globalFilters.data.find(gf => gf.id === "level") - if (!isShown.data) { - filter.filter = { - state: "*", - currentFilter: undefined, - } - filter.onNewPoint = 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 - } - 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)] - } - } - state.globalFilters.ping(); - return; - } - - - isShown.addCallbackAndRun(shown => { - console.log("Is level selector shown?", shown) - setLevelFilter() - if (shown) { - 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"))) + const levelSelector = new LevelSelector(state); + super([levelSelector, 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/FloorLevelInputElement.ts b/UI/Input/FloorLevelInputElement.ts new file mode 100644 index 000000000..844f05081 --- /dev/null +++ b/UI/Input/FloorLevelInputElement.ts @@ -0,0 +1,89 @@ +import {InputElement} from "./InputElement"; +import {Store, Stores, UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import Slider from "./Slider"; +import {ClickableToggle} from "./Toggle"; +import {FixedUiElement} from "../Base/FixedUiElement"; +import {VariableUiElement} from "../Base/VariableUIElement"; +import BaseUIElement from "../BaseUIElement"; + +export default class FloorLevelInputElement extends VariableUiElement implements InputElement { + + private readonly _value: UIEventSource; + + constructor(currentLevels: Store>, options?: { + value?: UIEventSource + }) { + + + const value = options?.value ?? new UIEventSource("0") + super(currentLevels.map(levels => { + const allLevels = Object.keys(levels) + allLevels.sort((a, b) => { + const an = Number(a) + const bn = Number(b) + if (isNaN(an) || isNaN(bn)) { + return a < b ? -1 : 1; + } + return an - bn; + }) + return FloorLevelInputElement.constructPicker(allLevels, value) + } + )) + + + this._value = value + + } + + private static constructPicker(levels: string[], value: UIEventSource): BaseUIElement { + let slider = new Slider(0, levels.length - 1, {vertical: true}); + 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("font-bold active bg-subtle " + toggleClass), + new FixedUiElement(data).SetClass("normal-background " + toggleClass), + slider.GetValue().sync( + (sliderVal) => { + return sliderVal === i + }, + [], + (isSelected) => { + return isSelected ? i : slider.GetValue().data + } + )) + .ToggleOnClick() + .SetClass("flex w-10 h-10")) + + 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().addCallbackD(i => { + if (levels === undefined) { + return + } + if(levels[i] == undefined){ + return + } + value.setData(levels[i]); + }) + value.addCallbackAndRunD(level => { + const i = levels.findIndex(l => l === level) + slider.GetValue().setData(i) + }) + return combine + } + + GetValue(): UIEventSource { + return this._value; + } + + IsValid(t: string): boolean { + return false; + } + + +} \ No newline at end of file diff --git a/UI/Input/LevelSelector.ts b/UI/Input/LevelSelector.ts deleted file mode 100644 index 766a1f6c8..000000000 --- a/UI/Input/LevelSelector.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {InputElement} from "./InputElement"; -import {Store, Stores, UIEventSource} from "../../Logic/UIEventSource"; -import Combine from "../Base/Combine"; -import Slider from "./Slider"; -import {ClickableToggle} from "./Toggle"; -import {FixedUiElement} from "../Base/FixedUiElement"; -import {VariableUiElement} from "../Base/VariableUIElement"; - -export default class LevelSelector extends VariableUiElement implements InputElement { - - private readonly _value: UIEventSource; - - constructor(currentLevels: Store, options?: { - value?: UIEventSource - }) { - 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}); - 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("font-bold active bg-subtle " + toggleClass), - new FixedUiElement(data).SetClass("normal-background " + toggleClass), - slider.GetValue().sync( - (sliderVal) => { - return sliderVal === i - }, - [], - (isSelected) => { - return isSelected ? i : slider.GetValue().data - } - )) - .ToggleOnClick() - .SetClass("flex w-10 h-10")) - - 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 - } - value.setData(currentLevels?.data[i]); - }) - value.addCallback(level => { - const i = currentLevels?.data?.findIndex(l => l === level) - slider.GetValue().setData(i) - }) - return combine - })) - - this._value = value - - } - - GetValue(): UIEventSource { - return this._value; - } - - IsValid(t: string): boolean { - return false; - } - - - -} \ No newline at end of file