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/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/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/State/MapState.ts b/Logic/State/MapState.ts index 5a2564c06..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 @@ -78,6 +91,12 @@ export default class MapState extends UserRelatedState { * Which layers are enabled in the current theme and what filters are applied onto them */ public filteredLayers: UIEventSource = new UIEventSource([], "filteredLayers"); + + /** + * Filters which apply onto all layers + */ + public globalFilters: UIEventSource = new UIEventSource([], "globalFilters") + /** * Which overlays are shown */ @@ -121,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() @@ -206,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() { @@ -335,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; @@ -354,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[] = []; @@ -363,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 2688d3140..aa91fa9b5 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 */ @@ -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. @@ -581,4 +591,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/BaseUIElement.ts b/UI/BaseUIElement.ts index 749f61d35..4c084d088 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -1,4 +1,4 @@ -import {Utils} from "../Utils"; +import { Utils } from "../Utils"; /** * A thin wrapper around a html element, which allows to generate a HTML-element. @@ -39,9 +39,9 @@ export default abstract class BaseUIElement { return this; } - - public ScrollToTop(){ - this._constructedHtmlElement?.scrollTo(0,0) + + public ScrollToTop() { + this._constructedHtmlElement?.scrollTo(0, 0) } /** @@ -70,10 +70,13 @@ export default abstract class BaseUIElement { return this; } - public RemoveClass(clss: string): BaseUIElement { - if (this.clss.has(clss)) { - this.clss.delete(clss); - this._constructedHtmlElement?.classList.remove(clss) + public RemoveClass(classes: string): BaseUIElement { + const all = classes.split(" ").map(clsName => clsName.trim()); + for (let clss of all) { + if (this.clss.has(clss)) { + this.clss.delete(clss); + this._constructedHtmlElement?.classList.remove(clss) + } } return this; } diff --git a/UI/BigComponents/RightControls.ts b/UI/BigComponents/RightControls.ts index 6e3e333fa..5edf81482 100644 --- a/UI/BigComponents/RightControls.ts +++ b/UI/BigComponents/RightControls.ts @@ -3,11 +3,22 @@ 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"; +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"; export default class RightControls extends Combine { - constructor(state: MapState) { + constructor(state: MapState & { featurePipeline: FeaturePipeline }) { const geolocatioHandler = new GeoLocationHandler( state @@ -38,7 +49,91 @@ export default class RightControls extends Combine { state.locationControl.ping(); }); - super([plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1"))) + 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"))) this.SetClass("flex flex-col items-center") } 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/Input/LevelSelector.ts b/UI/Input/LevelSelector.ts new file mode 100644 index 000000000..766a1f6c8 --- /dev/null +++ b/UI/Input/LevelSelector.ts @@ -0,0 +1,70 @@ +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 diff --git a/UI/Input/Slider.ts b/UI/Input/Slider.ts index 68503e722..e6e2f4a55 100644 --- a/UI/Input/Slider.ts +++ b/UI/Input/Slider.ts @@ -4,9 +4,10 @@ import {UIEventSource} from "../../Logic/UIEventSource"; export default class Slider extends InputElement { private readonly _value: UIEventSource - private min: number; - private max: number; - private step: number; + private readonly min: number; + private readonly max: number; + private readonly step: number; + private readonly vertical: boolean; /** * Constructs a slider input element for natural numbers @@ -16,13 +17,15 @@ export default class Slider extends InputElement { */ constructor(min: number, max: number, options?: { value?: UIEventSource, - step?: 1 | number + step?: 1 | number, + vertical?: false | boolean }) { super(); this.max = max; this.min = min; this._value = options?.value ?? new UIEventSource(min) this.step = options?.step ?? 1; + this.vertical = options?.vertical ?? false; } GetValue(): UIEventSource { @@ -39,6 +42,10 @@ export default class Slider extends InputElement { el.oninput = () => { valuestore.setData(Number(el.value)) } + if(this.vertical){ + el.classList.add("vertical") + el.setAttribute('orient','vertical'); // firefox only workaround... + } valuestore.addCallbackAndRunD(v => el.value = ""+valuestore.data) return el; } 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/assets/svg/elevator.svg b/assets/svg/elevator.svg new file mode 100644 index 000000000..ad43fe894 --- /dev/null +++ b/assets/svg/elevator.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + 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/assets/svg/license_info.json b/assets/svg/license_info.json index 9770e3ba7..6d1d8401e 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -387,9 +387,19 @@ ], "sources": [] }, + { + "path": "elevator.svg", + "license": "CC-BY-SA 4.0", + "authors": [ + "Yveltal" + ], + "sources": [ + "https://commons.wikimedia.org/wiki/File:HZM_elevator_icon.svg" + ] + }, { "path": "elevator_wheelchair.svg", - "license": "CC-BY_SA", + "license": "CC-BY-SA", "authors": [ "Robin Julien" ], diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 7cf5871e5..d242eee6c 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -1110,10 +1110,6 @@ video { height: 12rem; } -.max-h-screen { - max-height: 100vh; -} - .max-h-7 { max-height: 1.75rem; } @@ -1302,6 +1298,10 @@ video { flex-wrap: wrap-reverse; } +.place-content-center { + place-content: center; +} + .content-start { align-content: flex-start; } @@ -1481,6 +1481,11 @@ video { border-color: rgba(252, 165, 165, var(--tw-border-opacity)); } +.border-blue-500 { + --tw-border-opacity: 1; + border-color: rgba(59, 130, 246, var(--tw-border-opacity)); +} + .border-gray-200 { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); @@ -1510,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)); @@ -1530,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)); @@ -1857,15 +1862,11 @@ video { } .z-above-map { - z-index: 10000 + z-index: 10000; } .z-above-controls { - z-index: 10001 -} - -.w-160 { - width: 40rem; + z-index: 10001; } .bg-subtle { @@ -1892,14 +1893,14 @@ video { * Base colour of interactive elements, mainly the 'subtle button' * */ - --subtle-detail-color: #DBEAFE; + --subtle-detail-color: #dbeafe; --subtle-detail-color-contrast: black; --subtle-detail-color-light-contrast: lightgrey; /** * A stronger variant of the 'subtle-detail-colour' * Used as subtle button hover */ - --unsubtle-detail-color: #BFDBFE; + --unsubtle-detail-color: #bfdbfe; --unsubtle-detail-color-contrast: black; --catch-detail-color: #3a3aeb; --catch-detail-color-contrast: white; @@ -1913,7 +1914,8 @@ video { --variable-title-height: 0px; } -html, body { +html, +body { height: 100%; min-height: 100vh; min-height: -webkit-fill-available; @@ -1921,7 +1923,7 @@ html, body { padding: 0; background-color: var(--background-color); color: var(--foreground-color); - font-family: 'Helvetica Neue', Arial, sans-serif; + font-family: "Helvetica Neue", Arial, sans-serif; } .leaflet-overlay-pane .leaflet-zoom-animated { @@ -1944,7 +1946,8 @@ html, body { height: 100% !important; } -svg, img { +svg, +img { box-sizing: content-box; width: 100%; height: 100%; @@ -2048,6 +2051,35 @@ a { height: min-content; } +/* slider */ + +input[type="range"].vertical { + -webkit-writing-mode: bt-lr; + -ms-writing-mode: bt-lr; + writing-mode: bt-lr; + /* IE */ + -webkit-appearance: slider-vertical; + /* Chromium */ + cursor: pointer; +} + +@-moz-document url-prefix() { + 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; + cursor: pointer; + border-image: linear-gradient(to right, black 50%, transparent 50%) 100% 1; + padding-bottom: 5px; + } +} + .border-detail { border-color: var(--foreground-color); } @@ -2117,7 +2149,7 @@ p { } li::marker { - content: "•" + content: "•"; } .subtle-background { @@ -2127,7 +2159,7 @@ li::marker { .normal-background { background: var(--background-color); - color: var(--foreground-color) + color: var(--foreground-color); } .subtle-lighter { @@ -2205,7 +2237,8 @@ li::marker { color: unset !important; } -.disable-links a.must-link, .disable-links .must-link a { +.disable-links a.must-link, +.disable-links .must-link a { /* Hide links if they are disabled */ display: none; } @@ -2424,7 +2457,7 @@ li::marker { /***************** Info box (box containing features and questions ******************/ input { - color: var(--foreground-color) + color: var(--foreground-color); } .leaflet-popup-content { @@ -2485,7 +2518,7 @@ input { } .animate-height { - transition: max-height .5s ease-in-out; + transition: max-height 0.5s ease-in-out; overflow-y: hidden; } @@ -2549,7 +2582,7 @@ input { margin-left: 1rem; } -.mapping-icon-large{ +.mapping-icon-large { /* A mapping icon type */ width: 6rem; max-height: 5rem; @@ -2855,4 +2888,3 @@ input { display: inline; } } - diff --git a/index.css b/index.css index 61a1fa1f5..1db74d868 100644 --- a/index.css +++ b/index.css @@ -12,574 +12,583 @@ @tailwind utilities; @layer utilities { - @variants responsive { - .z-above-map { - z-index: 10000 - } - - .z-above-controls { - z-index: 10001 - } - - .w-160 { - width: 40rem; - } - - .bg-subtle { - background-color: var(--subtle-detail-color); - color: var(--subtle-detail-color-contrast); - } - - .bg-unsubtle { - background-color: var(--unsubtle-detail-color); - color: var(--unsubtle-detail-color-contrast); - } - - .bg-catch { - background-color: var(--catch-detail-color); - color: var(--catch-detail-color-contrast); - } - - .rounded-left-full { - border-bottom-left-radius: 999rem; - border-top-left-radius: 999rem; - } - - .rounded-right-full { - border-bottom-right-radius: 999rem; - border-top-right-radius: 999rem; - } + @variants responsive { + .z-above-map { + z-index: 10000; } + .z-above-controls { + z-index: 10001; + } + + .w-160 { + width: 40rem; + } + + .bg-subtle { + background-color: var(--subtle-detail-color); + color: var(--subtle-detail-color-contrast); + } + + .bg-unsubtle { + background-color: var(--unsubtle-detail-color); + color: var(--unsubtle-detail-color-contrast); + } + + .bg-catch { + background-color: var(--catch-detail-color); + color: var(--catch-detail-color-contrast); + } + + .rounded-left-full { + border-bottom-left-radius: 999rem; + border-top-left-radius: 999rem; + } + + .rounded-right-full { + border-bottom-right-radius: 999rem; + border-top-right-radius: 999rem; + } + } } - :root { - /* The main colour scheme of mapcomplete is configured here. + /* The main colour scheme of mapcomplete is configured here. * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. */ - /* Main color of the application: the background and text colours */ - --background-color: white; - /* Main text colour. Also styles some elements, such as the 'close popup'-button or 'back-arrow' (in mobile) */ - --foreground-color: black; - - /* A colour to indicate an error or warning */ - --alert-color: #fee4d1; - - /** + /* Main color of the application: the background and text colours */ + --background-color: white; + /* Main text colour. Also styles some elements, such as the 'close popup'-button or 'back-arrow' (in mobile) */ + --foreground-color: black; + + /* A colour to indicate an error or warning */ + --alert-color: #fee4d1; + + /** * Base colour of interactive elements, mainly the 'subtle button' * */ - --subtle-detail-color: #DBEAFE; - --subtle-detail-color-contrast: black; - --subtle-detail-color-light-contrast: lightgrey; + --subtle-detail-color: #dbeafe; + --subtle-detail-color-contrast: black; + --subtle-detail-color-light-contrast: lightgrey; - /** + /** * A stronger variant of the 'subtle-detail-colour' * Used as subtle button hover */ - --unsubtle-detail-color: #BFDBFE; - --unsubtle-detail-color-contrast: black; - - - --catch-detail-color: #3a3aeb; - --catch-detail-color-contrast: white; + --unsubtle-detail-color: #bfdbfe; + --unsubtle-detail-color-contrast: black; - - --non-active-tab-svg: var(--foreground-color); - --shadow-color: #00000066; - - --return-to-the-map-height: 2em; - --image-carousel-height: 350px; + --catch-detail-color: #3a3aeb; + --catch-detail-color-contrast: white; - /* The border colour of the leaflet popup */ - --popup-border: white; + --non-active-tab-svg: var(--foreground-color); + --shadow-color: #00000066; - /* Technical variable to make some dynamic behaviour possible; set by javascript. */ - --variable-title-height: 0px; + --return-to-the-map-height: 2em; + --image-carousel-height: 350px; + + /* The border colour of the leaflet popup */ + --popup-border: white; + + /* Technical variable to make some dynamic behaviour possible; set by javascript. */ + --variable-title-height: 0px; } -html, body { - height: 100%; - min-height: 100vh; - min-height: -webkit-fill-available; - margin: 0; - padding: 0; - background-color: var(--background-color); - color: var(--foreground-color); - font-family: 'Helvetica Neue', Arial, sans-serif; +html, +body { + height: 100%; + min-height: 100vh; + min-height: -webkit-fill-available; + margin: 0; + padding: 0; + background-color: var(--background-color); + color: var(--foreground-color); + font-family: "Helvetica Neue", Arial, sans-serif; } .leaflet-overlay-pane .leaflet-zoom-animated { - /* Another workaround to keep leaflet working */ - width: initial !important; - height: initial !important; - box-sizing: initial !important; + /* Another workaround to keep leaflet working */ + width: initial !important; + height: initial !important; + box-sizing: initial !important; } .leaflet-control-attribution { - display: block ruby; + display: block ruby; } .badge { } .badge svg { - /*Workaround for leaflet*/ - width: unset !important; - height: 100% !important; + /*Workaround for leaflet*/ + width: unset !important; + height: 100% !important; } -svg, img { - box-sizing: content-box; - width: 100%; - height: 100%; +svg, +img { + box-sizing: content-box; + width: 100%; + height: 100%; } .titleicon img { - width: unset; + width: unset; } .titleicon svg { - width: unset; + width: unset; } .svg-catch svg path { - fill: var(--catch-detail-color) !important; - stroke: var(--catch-detail-color) !important; + fill: var(--catch-detail-color) !important; + stroke: var(--catch-detail-color) !important; } .svg-unsubtle svg path { - fill: var(--unsubtle-detail-color) !important; - stroke: var(--unsubtle-detail-color) !important; + fill: var(--unsubtle-detail-color) !important; + stroke: var(--unsubtle-detail-color) !important; } .svg-subtle svg path { - fill: var(--subtle-detail-color) !important; - stroke: var(--subtle-detail-color) !important; + fill: var(--subtle-detail-color) !important; + stroke: var(--subtle-detail-color) !important; } .svg-foreground svg path { - fill: var(--foreground-color) !important; - stroke: var(--foreground-color) !important; + fill: var(--foreground-color) !important; + stroke: var(--foreground-color) !important; } .no-images img { - display: none; + display: none; } .weblate-link { - /* Weblate-links are the little translation icon next to translatable sentences. Due to their special nature, they are exempt from some rules */ + /* Weblate-links are the little translation icon next to translatable sentences. Due to their special nature, they are exempt from some rules */ } .mapcontrol svg path { - fill: var(--subtle-detail-color-contrast) !important; + fill: var(--subtle-detail-color-contrast) !important; } .red-svg svg path { - stroke: #d71010 !important; + stroke: #d71010 !important; } a { - color: var(--foreground-color); + color: var(--foreground-color); } .btn { - line-height: 1.25rem; - --tw-text-opacity: 1; - color: var(--catch-detail-color-contrast); - --tw-bg-opacity: 1; - background-color: var(--catch-detail-color); - display: inline-flex; - border-radius: 1.5rem; - padding-top: 0.75rem; - padding-bottom: 0.75rem; - padding-left: 1.25rem; - padding-right: 1.25rem; - font-size: large; - font-weight: bold; - transition: 100ms; - /*-- invisible border: rendered on hover*/ - border: 3px solid var(--unsubtle-detail-color); + line-height: 1.25rem; + --tw-text-opacity: 1; + color: var(--catch-detail-color-contrast); + --tw-bg-opacity: 1; + background-color: var(--catch-detail-color); + display: inline-flex; + border-radius: 1.5rem; + padding-top: 0.75rem; + padding-bottom: 0.75rem; + padding-left: 1.25rem; + padding-right: 1.25rem; + font-size: large; + font-weight: bold; + transition: 100ms; + /*-- invisible border: rendered on hover*/ + border: 3px solid var(--unsubtle-detail-color); } .btn:hover { - border: 3px solid var(--catch-detail-color); + border: 3px solid var(--catch-detail-color); } .btn-secondary { - background-color: var(--catch-detail-color); - filter: saturate(0.5); - + background-color: var(--catch-detail-color); + filter: saturate(0.5); } .btn-secondary:hover { - background-color: var(--catch-detail-color); - filter: unset; + background-color: var(--catch-detail-color); + filter: unset; } .btn-disabled { - filter: saturate(0.3); - cursor: default; + filter: saturate(0.3); + cursor: default; } .btn-disabled:hover { - border: 3px solid var(--unsubtle-detail-color); + border: 3px solid var(--unsubtle-detail-color); } .h-min { - height: min-content; + height: min-content; } +/* slider */ +input[type="range"].vertical { + writing-mode: bt-lr; /* IE */ + -webkit-appearance: slider-vertical; /* Chromium */ + cursor: pointer; +} + +@-moz-document url-prefix() { + 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; + cursor: pointer; + border-image: linear-gradient(to right, black 50%, transparent 50%) 100% 1; + padding-bottom: 5px; + } +} .border-detail { - border-color: var(--foreground-color); + border-color: var(--foreground-color); } .w-min { - width: min-content; + width: min-content; } .rounded-left-full { - border-bottom-left-radius: 999rem; - border-top-left-radius: 999rem; + border-bottom-left-radius: 999rem; + border-top-left-radius: 999rem; } .rounded-right-full { - border-bottom-right-radius: 999rem; - border-top-right-radius: 999rem; + border-bottom-right-radius: 999rem; + border-top-right-radius: 999rem; } .w-16-imp { - width: 4rem !important; + width: 4rem !important; } .w-32-imp { - width: 8rem !important; + width: 8rem !important; } .w-48-imp { - width: 12rem !important; + width: 12rem !important; } .link-underline a { - text-decoration: underline 1px var(--foreground-color); + text-decoration: underline 1px var(--foreground-color); } .link-no-underline a { - text-decoration: none; + text-decoration: none; } li { - margin-left: 0.5em; - padding-left: 0.2em; - margin-top: 0.1em; + margin-left: 0.5em; + padding-left: 0.2em; + margin-top: 0.1em; } h2 { - font-size: large; - margin-top: 0.5em; - margin-bottom: 0.3em; - font-weight: bold; + font-size: large; + margin-top: 0.5em; + margin-bottom: 0.3em; + font-weight: bold; } h3 { - font-size: larger; - margin-top: 0.6em; - margin-bottom: 0; - font-weight: bold; + font-size: larger; + margin-top: 0.6em; + margin-bottom: 0; + font-weight: bold; } h3 { - font-size: larger; - margin-top: 0.6em; - margin-bottom: 0; - font-weight: bolder; + font-size: larger; + margin-top: 0.6em; + margin-bottom: 0; + font-weight: bolder; } p { - padding-top: 0.1em; + padding-top: 0.1em; } li::marker { - content: "•" + content: "•"; } .subtle-background { - background: var(--subtle-detail-color); - color: var(--subtle-detail-color-contrast); + background: var(--subtle-detail-color); + color: var(--subtle-detail-color-contrast); } .normal-background { - background: var(--background-color); - color: var(--foreground-color) + background: var(--background-color); + color: var(--foreground-color); } .subtle-lighter { - color: var(--subtle-detail-color-light-contrast); + color: var(--subtle-detail-color-light-contrast); } .border-attention-catch { - border: 5px solid var(--catch-detail-color); + border: 5px solid var(--catch-detail-color); } .border-invisible { - border: 5px solid #00000000; + border: 5px solid #00000000; } .border-attention { - border-color: var(--catch-detail-color); + border-color: var(--catch-detail-color); } .direction-svg svg path { - fill: var(--catch-detail-color) !important; + fill: var(--catch-detail-color) !important; } - #leafletDiv { - height: 100%; + height: 100%; } .leaflet-popup-content-wrapper { - background-color: var(--background-color); - color: var(--foreground-color); - border: 2px solid var(--popup-border); - box-shadow: 0 3px 14px var(--shadow-color) !important; + background-color: var(--background-color); + color: var(--foreground-color); + border: 2px solid var(--popup-border); + box-shadow: 0 3px 14px var(--shadow-color) !important; } .leaflet-container { - font: unset !important; - background-color: var(--background-color) !important; + font: unset !important; + background-color: var(--background-color) !important; } .leaflet-popup-tip { - background-color: var(--popup-border) !important; - color: var(--popup-border) !important; - box-shadow: 0 3px 14px var(--shadow-color) !important; + background-color: var(--popup-border) !important; + color: var(--popup-border) !important; + box-shadow: 0 3px 14px var(--shadow-color) !important; } .single-layer-selection-toggle { - position: relative; - width: 2em; - height: 2em; - flex-shrink: 0; + position: relative; + width: 2em; + height: 2em; + flex-shrink: 0; } .single-layer-selection-toggle img { - max-height: 2em !important; - max-width: 2em !important; + max-height: 2em !important; + max-width: 2em !important; } .single-layer-selection-toggle svg { - max-height: 2em !important; - max-width: 2em !important; + max-height: 2em !important; + max-width: 2em !important; } - .block-ruby { - display: block ruby; + display: block ruby; } .disable-links a { - pointer-events: none; - text-decoration: none !important; - color: var(--subtle-detail-color-contrast) !important; + pointer-events: none; + text-decoration: none !important; + color: var(--subtle-detail-color-contrast) !important; } .enable-links a { - pointer-events: unset; - text-decoration: underline !important; - color: unset !important; + pointer-events: unset; + text-decoration: underline !important; + color: unset !important; } -.disable-links a.must-link, .disable-links .must-link a { - /* Hide links if they are disabled */ - display: none; +.disable-links a.must-link, +.disable-links .must-link a { + /* Hide links if they are disabled */ + display: none; } /**************** GENERIC ****************/ - .alert { - background-color: var(--alert-color); - color: var(--foreground-color); - font-weight: bold; - border-radius: 1em; - margin: 0.25em; - text-align: center; - padding: 0.15em 0.3em; + background-color: var(--alert-color); + color: var(--foreground-color); + font-weight: bold; + border-radius: 1em; + margin: 0.25em; + text-align: center; + padding: 0.15em 0.3em; } .invalid { - box-shadow: 0 0 10px #ff5353; - height: min-content; + box-shadow: 0 0 10px #ff5353; + height: min-content; } .shadow { - box-shadow: 0 0 10px var(--shadow-color); + box-shadow: 0 0 10px var(--shadow-color); } .title-font span { - font-size: xx-large !important; - font-weight: bold; + font-size: xx-large !important; + font-weight: bold; } .soft { - background-color: var(--subtle-detail-color); - color: var(--subtle-detail-color-contrast); - font-weight: bold; - border-radius: 1em; - margin: 0.25em; - text-align: center; - padding: 0.15em 0.3em; + background-color: var(--subtle-detail-color); + color: var(--subtle-detail-color-contrast); + font-weight: bold; + border-radius: 1em; + margin: 0.25em; + text-align: center; + padding: 0.15em 0.3em; } - .subtle { - color: #999; + color: #999; } .link-underline .subtle a { - text-decoration: underline 1px #7193bb88; - color: #7193bb; + text-decoration: underline 1px #7193bb88; + color: #7193bb; } - .thanks { - background-color: #43d904; - font-weight: bold; - border-radius: 1em; - margin: 0.25em; - text-align: center; - padding: 0.15em 0.3em; + background-color: #43d904; + font-weight: bold; + border-radius: 1em; + margin: 0.25em; + text-align: center; + padding: 0.15em 0.3em; } .clickable { - pointer-events: all; + pointer-events: all; } .unclickable { - pointer-events: none !important; + pointer-events: none !important; } - @keyframes slide { - /* This is the animation on the marker to add a new point - it slides through all the possible presets */ - from { - transform: translateX(0%); - } + /* This is the animation on the marker to add a new point - it slides through all the possible presets */ + from { + transform: translateX(0%); + } - to { - transform: translateX(calc(-100% + 42px)); - } + to { + transform: translateX(calc(-100% + 42px)); + } } .hand-drag-animation { - animation: hand-drag-animation 6s ease-in-out infinite; - transform-origin: 50% 125%; + animation: hand-drag-animation 6s ease-in-out infinite; + transform-origin: 50% 125%; } @keyframes hand-drag-animation { - /* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */ - 0% { - opacity: 0; - transform: rotate(-30deg); - } + /* This is the animation on the little extra hand on the location input. If fades in, invites the user to interact/drag the map */ + 0% { + opacity: 0; + transform: rotate(-30deg); + } - 6% { - opacity: 1; - transform: rotate(-30deg); - } + 6% { + opacity: 1; + transform: rotate(-30deg); + } - 12% { - opacity: 1; - transform: rotate(-45deg); - } + 12% { + opacity: 1; + transform: rotate(-45deg); + } - 24% { - opacity: 1; - transform: rotate(-00deg); - } + 24% { + opacity: 1; + transform: rotate(-00deg); + } - 30% { - opacity: 1; - transform: rotate(-30deg); - } + 30% { + opacity: 1; + transform: rotate(-30deg); + } + 36% { + opacity: 0; + transform: rotate(-30deg); + } - 36% { - opacity: 0; - transform: rotate(-30deg); - } - - 100% { - opacity: 0; - transform: rotate(-30deg); - } - + 100% { + opacity: 0; + transform: rotate(-30deg); + } } /**************************************/ - #topleft-tools { - display: block; - position: absolute; - z-index: 5000; - transition: all 500ms linear; - left: 0; - right: 0; + display: block; + position: absolute; + z-index: 5000; + transition: all 500ms linear; + left: 0; + right: 0; } .welcomeMessage { - display: block; - max-width: calc(100vw - 5em); - width: 40em; - max-height: calc(100vh - 15em); - background-color: var(--background-color); - color: var(--foreground-color); + display: block; + max-width: calc(100vw - 5em); + width: 40em; + max-height: calc(100vh - 15em); + background-color: var(--background-color); + color: var(--foreground-color); } - - /***************** Info box (box containing features and questions ******************/ input { - color: var(--foreground-color) + color: var(--foreground-color); } .leaflet-popup-content { - width: 45em !important; - margin: 0.25rem !important; + width: 45em !important; + margin: 0.25rem !important; } .leaflet-div-icon { - background-color: unset !important; - border: unset !important; + background-color: unset !important; + border: unset !important; } .floating-element-width { - max-width: calc(100vw - 5em); - width: 40em; + max-width: calc(100vw - 5em); + width: 40em; } .leaflet-div-icon svg { - width: calc(100%); - height: calc(100%); + width: calc(100%); + height: calc(100%); } /****** ShareScreen *****/ .literal-code { - display: inline-block; - background-color: lightgray; - padding: 0.5em; - word-break: break-word; - color: black; - box-sizing: border-box; + display: inline-block; + background-color: lightgray; + padding: 0.5em; + word-break: break-word; + color: black; + box-sizing: border-box; } - .code { display: inline-block; background-color: lightgray; @@ -591,97 +600,91 @@ input { /** Switch layout **/ .small-image img { - height: 1em; - max-width: 1em; + height: 1em; + max-width: 1em; } .small-image { - height: 1em; - max-width: 1em; + height: 1em; + max-width: 1em; } - .slideshow-item img { - height: var(--image-carousel-height); - width: unset; + height: var(--image-carousel-height); + width: unset; } .animate-height { - transition: max-height .5s ease-in-out; - overflow-y: hidden; + transition: max-height 0.5s ease-in-out; + overflow-y: hidden; } - .zebra-table tr:nth-child(even) { - background-color: #f2f2f2; + background-color: #f2f2f2; } .layer-toggle { - /* The checkbox that toggles a single layer */ + /* The checkbox that toggles a single layer */ } .layer-filters { - /* If needed, the panel which contains the extra filters for a layer */ - margin-bottom: 1rem; - border-bottom: 2px solid var(--foreground-color); + /* If needed, the panel which contains the extra filters for a layer */ + margin-bottom: 1rem; + border-bottom: 2px solid var(--foreground-color); } .filter-panel { - /* The panel for a single layer, containing both the toggle and the filters (if any) */ - border-bottom: 2px solid lightgrey; - margin-bottom: 0.5rem; + /* The panel for a single layer, containing both the toggle and the filters (if any) */ + border-bottom: 2px solid lightgrey; + margin-bottom: 0.5rem; } .first-filter-panel { - /* Additional class on the first layer filter */ + /* Additional class on the first layer filter */ } .mapping-icon-small-height { - /* A mapping icon type */ - height: 1.5rem; - margin-right: 0.5rem; - width: unset; + /* A mapping icon type */ + height: 1.5rem; + margin-right: 0.5rem; + width: unset; } .mapping-icon-medium-height { - /* A mapping icon type */ - height: 3rem; - margin-right: 0.5rem; - width: unset; + /* A mapping icon type */ + height: 3rem; + margin-right: 0.5rem; + width: unset; } .mapping-icon-large-height { - /* A mapping icon type */ - height: 5rem; - margin-right: 0.5rem; - width: unset; + /* A mapping icon type */ + height: 5rem; + margin-right: 0.5rem; + width: unset; } - .mapping-icon-small { - /* A mapping icon type */ - width: 1.5rem; - max-height: 1.5rem; - margin-right: 0.5rem; + /* A mapping icon type */ + width: 1.5rem; + max-height: 1.5rem; + margin-right: 0.5rem; } .mapping-icon-medium { - /* A mapping icon type */ - width: 3rem; - max-height: 3rem; - margin-right: 1rem; - margin-left: 1rem; + /* A mapping icon type */ + width: 3rem; + max-height: 3rem; + margin-right: 1rem; + margin-left: 1rem; } -.mapping-icon-large{ - /* A mapping icon type */ - width: 6rem; - max-height: 5rem; - margin-top: 0.5rem; - margin-bottom: 0.5rem; - margin-right: 1.5rem; - margin-left: 1.5rem; - - +.mapping-icon-large { + /* A mapping icon type */ + width: 6rem; + max-height: 5rem; + margin-top: 0.5rem; + margin-bottom: 0.5rem; + margin-right: 1.5rem; + margin-left: 1.5rem; } - 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", diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index ce791b189..e1cb4415d 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -10,7 +10,7 @@ import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; import RelationsTracker from "../Logic/Osm/RelationsTracker"; import * as OsmToGeoJson from "osmtogeojson"; import MetaTagging from "../Logic/MetaTagging"; -import {UIEventSource} from "../Logic/UIEventSource"; +import {ImmutableStore, UIEventSource} from "../Logic/UIEventSource"; import {TileRange, Tiles} from "../Models/TileRange"; import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; import ScriptUtils from "./ScriptUtils"; @@ -250,9 +250,10 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations } const filteredTile = new FilteringFeatureSource({ - locationControl: new UIEventSource(undefined), + locationControl: new ImmutableStore(undefined), allElements: undefined, - selectedElement: new UIEventSource(undefined) + selectedElement: new ImmutableStore(undefined), + globalFilters: new ImmutableStore([]) }, tileIndex, tile, @@ -323,9 +324,10 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations if (pointsOnlyLayers.indexOf(layer.id) >= 0) { const filtered = new FilteringFeatureSource({ - locationControl: new UIEventSource(undefined), + locationControl: new ImmutableStore(undefined), allElements: undefined, - selectedElement: new UIEventSource(undefined) + selectedElement: new ImmutableStore(undefined), + globalFilters: new ImmutableStore([]) }, Tiles.tile_index(0, 0, 0), source, diff --git a/test.ts b/test.ts index f9feaf0fa..f6a656f45 100644 --- a/test.ts +++ b/test.ts @@ -1,76 +1,4 @@ -import ChartJs from "./UI/Base/ChartJs"; -import TagRenderingChart from "./UI/BigComponents/TagRenderingChart"; -import {OsmFeature} from "./Models/OsmFeature"; -import * as food from "./assets/generated/layers/food.json" -import TagRenderingConfig from "./Models/ThemeConfig/TagRenderingConfig"; +import LevelSelector from "./UI/Input/LevelSelector"; import {UIEventSource} from "./Logic/UIEventSource"; -import Combine from "./UI/Base/Combine"; -const data = new UIEventSource([ - { - properties: { - id: "node/1234", - cuisine:"pizza", - "payment:cash":"yes" - }, - geometry:{ - type: "Point", - coordinates: [0,0] - }, - id: "node/1234", - type: "Feature" - }, - { - properties: { - id: "node/42", - cuisine:"pizza", - "payment:cash":"yes" - }, - geometry:{ - type: "Point", - coordinates: [1,0] - }, - id: "node/42", - type: "Feature" - }, - { - properties: { - id: "node/452", - cuisine:"pasta", - "payment:cash":"yes", - "payment:cards":"yes" - }, - geometry:{ - type: "Point", - coordinates: [2,0] - }, - id: "node/452", - type: "Feature" - }, - { - properties: { - id: "node/4542", - cuisine:"something_comletely_invented", - "payment:cards":"yes" - }, - geometry:{ - type: "Point", - coordinates: [3,0] - }, - id: "node/4542", - type: "Feature" - }, - { - properties: { - id: "node/45425", - }, - geometry:{ - type: "Point", - coordinates: [3,0] - }, - id: "node/45425", - type: "Feature" - } -]); -new Combine(food.tagRenderings.map(tr => new TagRenderingChart(data, new TagRenderingConfig(tr, "test"), {chartclasses: "w-160 h-160"}))) - .AttachTo("maindiv") \ No newline at end of file +new LevelSelector(new UIEventSource(["0","1","2","2.5","x","3"])).AttachTo("maindiv") \ No newline at end of file