forked from MapComplete/MapComplete
		
	
							parent
							
								
									5505a896bc
								
							
						
					
					
						commit
						4473560391
					
				
					 5 changed files with 234 additions and 162 deletions
				
			
		|  | @ -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]) | ||||
|  |  | |||
							
								
								
									
										140
									
								
								UI/BigComponents/LevelSelector.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								UI/BigComponents/LevelSelector.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<string, number>> = 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<string, number> = {"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]) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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") | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										89
									
								
								UI/Input/FloorLevelInputElement.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								UI/Input/FloorLevelInputElement.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<string> { | ||||
| 
 | ||||
|     private readonly _value: UIEventSource<string>; | ||||
| 
 | ||||
|     constructor(currentLevels: Store<Record<string, number>>, options?: { | ||||
|         value?: UIEventSource<string> | ||||
|     }) { | ||||
| 
 | ||||
| 
 | ||||
|         const value = options?.value ?? new UIEventSource<string>("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<string>): 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<string> { | ||||
|         return this._value; | ||||
|     } | ||||
| 
 | ||||
|     IsValid(t: string): boolean { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -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<string> { | ||||
| 
 | ||||
|     private readonly _value: UIEventSource<string>; | ||||
| 
 | ||||
|     constructor(currentLevels: Store<string[]>, options?: { | ||||
|         value?: UIEventSource<string> | ||||
|     }) { | ||||
|         const value = options?.value ?? new UIEventSource<string>(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<string> { | ||||
|         return this._value; | ||||
|     } | ||||
| 
 | ||||
|     IsValid(t: string): boolean { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue