diff --git a/InitUiElements.ts b/InitUiElements.ts index 1fe16d6..b8a96e9 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -1,6 +1,5 @@ import {FixedUiElement} from "./UI/Base/FixedUiElement"; import CheckBox from "./UI/Input/CheckBox"; -import Combine from "./UI/Base/Combine"; import {Basemap} from "./UI/BigComponents/Basemap"; import State from "./State"; import LoadFromOverpass from "./Logic/Actors/UpdateFromOverpass"; @@ -29,12 +28,12 @@ import LayerResetter from "./Logic/Actors/LayerResetter"; import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs"; import LayerControlPanel from "./UI/BigComponents/LayerControlPanel"; import FeatureSwitched from "./UI/Base/FeatureSwitched"; -import LayerConfig from "./Customizations/JSON/LayerConfig"; import ShowDataLayer from "./UI/ShowDataLayer"; import Hash from "./Logic/Web/Hash"; import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; -import HashHandler from "./Logic/Actors/SelectedFeatureHandler"; import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; +import ScrollableFullScreen from "./UI/Base/ScrollableFullScreen"; +import Translations from "./UI/i18n/Translations"; export class InitUiElements { @@ -217,10 +216,10 @@ export class InitUiElements { // ?-Button on Desktop, opens panel with close-X. const help = Svg.help_svg().SetClass("open-welcome-button block"); const checkbox = new CheckBox( - fullOptions - .SetClass("welcomeMessage") - .onClick(() => {/*Catch the click*/ - }), + fullOptions + .SetClass("welcomeMessage") + .onClick(() => {/*Catch the click*/ + }), help , isOpened ).AttachTo("messagesbox"); @@ -247,8 +246,8 @@ export class InitUiElements { const layerControlPanel = new LayerControlPanel( () => State.state.layerControlIsOpened.setData(false)) .SetClass("block p-1 rounded-full"); - const checkbox = new CheckBox( - layerControlPanel, + const checkbox = new CheckBox( + layerControlPanel, Svg.layers_svg().SetClass("layer-selection-toggle"), State.state.layerControlIsOpened ).AttachTo("layer-selection"); @@ -261,7 +260,7 @@ export class InitUiElements { }); State.state.selectedElement.addCallbackAndRun(feature => { - if(feature !== undefined){ + if (feature !== undefined) { checkbox.isEnabled.setData(false); } }) @@ -306,28 +305,27 @@ export class InitUiElements { const state = State.state; - const flayers: { layerDef: LayerConfig, isDisplayed: UIEventSource }[] = [] - for (const layer of state.layoutToUse.data.layers) { + state.filteredLayers = + state.layoutToUse.map(layoutToUse => { + const flayers = []; - if (typeof (layer) === "string") { - throw "Layer " + layer + " was not substituted"; - } + for (const layer of layoutToUse.layers) { - const isDisplayed = QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wether or not layer " + layer.id + " is shown") - .map((str) => str !== "false", [], (b) => b.toString()); - const flayer = { - isDisplayed: isDisplayed, - layerDef: layer - } - flayers.push(flayer); - } - - State.state.filteredLayers.setData(flayers); + const isDisplayed = QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wether or not layer " + layer.id + " is shown") + .map((str) => str !== "false", [], (b) => b.toString()); + const flayer = { + isDisplayed: isDisplayed, + layerDef: layer + } + flayers.push(flayer); + } + return flayers; + }); const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); State.state.layerUpdater = updater; - const source = new FeaturePipeline(flayers, updater, state.layoutToUse); + const source = new FeaturePipeline(state.filteredLayers.data, updater, state.layoutToUse, state.changes, state.locationControl); source.features.addCallbackAndRun((featuresFreshness: { feature: any, freshness: Date }[]) => { @@ -337,18 +335,18 @@ export class InitUiElements { let features = featuresFreshness.map(ff => ff.feature); features.forEach(feature => { State.state.allElements.addOrGetElement(feature); - - if(Hash.hash.data === feature.properties.id.replace("/","_")){ + + if (Hash.hash.data === feature.properties.id.replace("/", "_")) { State.state.selectedElement.setData(feature); } - + }) MetaTagging.addMetatags(features); }) new ShowDataLayer(source.features, State.state.leafletMap, - State.state.layoutToUse.data); - + State.state.layoutToUse); + new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source); @@ -383,11 +381,11 @@ export class InitUiElements { State.state.selectedElement, State.state.filteredLayers, State.state.leafletMap, - () => { - return new SimpleAddUI( - () => State.state.LastClickLocation.setData(undefined) - ); - } + () => + new ScrollableFullScreen( + Translations.t.general.add.title, + new SimpleAddUI(), + () => State.state.LastClickLocation.setData(undefined)) ); }); diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts index 2e68a35..5c4a34d 100644 --- a/Logic/Actors/SelectedFeatureHandler.ts +++ b/Logic/Actors/SelectedFeatureHandler.ts @@ -44,10 +44,12 @@ export default class SelectedFeatureHandler { // Feature already selected return; } + console.log("Selecting a feature from the hash...") for (const feature of features) { const id = feature.feature?.properties?.id; if(id === this._hash.data){ this._selectedFeature.setData(feature.feature); + break; } } } diff --git a/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts b/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts index 8b35ba3..1ac8be7 100644 --- a/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts +++ b/Logic/FeatureSource/FeatureDuplicatorPerLayer.ts @@ -52,9 +52,6 @@ export default class FeatureDuplicatorPerLayer implements FeatureSource { } } } - if(!foundALayer){ - console.error("LAYER DEDUP PANIC: no suitable layer found for ", f, JSON.stringify(f), "within layers", layers) - } } return newFeatures; diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index ea2e9b2..ef7ff6f 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -1,5 +1,4 @@ import FilteringFeatureSource from "../FeatureSource/FilteringFeatureSource"; -import State from "../../State"; import FeatureSourceMerger from "../FeatureSource/FeatureSourceMerger"; import RememberingSource from "../FeatureSource/RememberingSource"; import WayHandlingApplyingFeatureSource from "../FeatureSource/WayHandlingApplyingFeatureSource"; @@ -11,6 +10,7 @@ import LocalStorageSaver from "./LocalStorageSaver"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; import LocalStorageSource from "./LocalStorageSource"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; +import Loc from "../../Models/Loc"; export default class FeaturePipeline implements FeatureSource { @@ -18,7 +18,9 @@ export default class FeaturePipeline implements FeatureSource { constructor(flayers: { isDisplayed: UIEventSource, layerDef: LayerConfig }[], updater: FeatureSource, - layout: UIEventSource) { + layout: UIEventSource, + newPoints: FeatureSource, + locationControl: UIEventSource) { const amendedOverpassSource = new RememberingSource( @@ -34,15 +36,18 @@ export default class FeaturePipeline implements FeatureSource { new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout))) )); + newPoints = new FeatureDuplicatorPerLayer(flayers, newPoints); + const merged = new FeatureSourceMerger([ amendedOverpassSource, - new FeatureDuplicatorPerLayer(flayers, State.state.changes), - amendedLocalStorageSource + amendedLocalStorageSource, + newPoints ]); + const source = new FilteringFeatureSource( flayers, - State.state.locationControl, + locationControl, merged ); this.features = source.features; diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts index c044aa2..d63d412 100644 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -13,29 +13,35 @@ export default class FilteringFeatureSource implements FeatureSource { location: UIEventSource, upstream: FeatureSource) { - const layerDict = {}; - const self = this; + + const layerDict = {}; + for (const layer of layers) { + layerDict[layer.layerDef.id] = layer; + } + function update() { + console.log("Updating the filtering layer") const features: { feature: any, freshness: Date }[] = upstream.features.data; + + const newFeatures = features.filter(f => { const layerId = f.feature._matching_layer_id; - if (layerId === undefined) { - console.error(f) - throw "feature._matching_layer_id is undefined" + if (layerId !== undefined) { + const layer: { + isDisplayed: UIEventSource, + layerDef: LayerConfig + } = layerDict[layerId]; + if (layer === undefined) { + console.error("No layer found with id ", layerId); + return true; + } + if (FilteringFeatureSource.showLayer(layer, location)) { + return true; + } } - const layer: { - isDisplayed: UIEventSource, - layerDef: LayerConfig - } = layerDict[layerId]; - if (layer === undefined) { - throw "No layer found with id " + layerId; - } - if (FilteringFeatureSource.showLayer(layer, location)) { - return true; - } - // Does it match any other layer? + // Does it match any other layer - e.g. because of a switch? for (const toCheck of layers) { if (!FilteringFeatureSource.showLayer(toCheck, location)) { continue; @@ -50,9 +56,7 @@ export default class FilteringFeatureSource implements FeatureSource { self.features.setData(newFeatures); } - for (const layer of layers) { - layerDict[layer.layerDef.id] = layer; - } + upstream.features.addCallback(() => { update() }); diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index cf9e990..8584109 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -4,7 +4,6 @@ import opening_hours from "opening_hours"; import {And, Or, Tag} from "./Tags"; import {Utils} from "../Utils"; import CountryCoder from "latlon2country" -import {UIEventSource} from "./UIEventSource"; class SimpleMetaTagger { public readonly keys: string[]; @@ -135,7 +134,7 @@ export default class MetaTagging { } updateTags(); } catch (e) { - console.error("Error while parsing opening hours of ", tags.id, e); + console.warn("Error while parsing opening hours of ", tags.id, e); tags["_isOpen"] = "parse_error"; } diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 4a5e622..05be3f1 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -100,10 +100,12 @@ export class Changes implements FeatureSource{ } changes.push({elementId: id, key: kv.key, value: kv.value}) } - State.state.allElements.addOrGetElement(geojson).ping(); - + + console.log("New feature added and pinged") this.features.data.push({feature:geojson, freshness: new Date()}); this.features.ping(); + + State.state.allElements.addOrGetElement(geojson).ping(); this.uploadAll([osmNode], changes); return geojson; diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index aa7ef8e..d372960 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -77,7 +77,7 @@ export class UIEventSource { } public setData(t: T): UIEventSource { - if (this.data === t) { + if (this.data == t) { // MUST COMPARE BY REFERENCE! return; } this.data = t; @@ -86,8 +86,12 @@ export class UIEventSource { } public ping(): void { + const old = this.data; for (const callback of this._callbacks) { callback(this.data); + if(this.data === undefined && old !== undefined){ + throw "Something undefined the data!" + } } } diff --git a/UI/Base/ScrollableFullScreen.ts b/UI/Base/ScrollableFullScreen.ts index 8c751ff..f78549c 100644 --- a/UI/Base/ScrollableFullScreen.ts +++ b/UI/Base/ScrollableFullScreen.ts @@ -21,6 +21,7 @@ export default class ScrollableFullScreen extends UIElement { Svg.close_svg().SetClass("hidden md:block") ]) .onClick(() => { + console.log("Closing...") ScrollableFullScreen.RestoreLeaflet(); if (onClose !== undefined) { onClose(); @@ -92,6 +93,19 @@ export default class ScrollableFullScreen extends UIElement { if(htmlElement === undefined || htmlElement === null){ return; } + + let toHide = document.getElementsByClassName("leaflet-pane"); + for (let i = 0; i < toHide.length; ++i) { + toHide[i].classList.add("no-transform"); + toHide[i].classList.add("scrollable-fullscreen-no-transform"); + } + + toHide = document.getElementsByClassName("leaflet-popup"); + for (let i = 0; i < toHide.length; ++i) { + toHide[i].classList.add("no-transform"); + toHide[i].classList.add("scrollable-fullscreen-no-transform"); + } + do { // A leaflet workaround: in order for fullscreen to work, we need to get the parent element which does a transform3d and remove/read the transform if (htmlElement.style.transform !== "") { diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index cd8898d..6d3b095 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -13,7 +13,6 @@ import {FixedUiElement} from "../Base/FixedUiElement"; import Translations from "../i18n/Translations"; import Constants from "../../Models/Constants"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; -import ScrollableFullScreen from "../Base/ScrollableFullScreen"; export default class SimpleAddUI extends UIElement { private readonly _loginButton: UIElement; @@ -37,7 +36,7 @@ export default class SimpleAddUI extends UIElement { private readonly goToInboxButton: UIElement = new SubtleButton(Svg.envelope_ui(), Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false}); - constructor(onClose: () => void) { + constructor() { super(State.state.locationControl.map(loc => loc.zoom)); const self = this; this.ListenTo(Locale.language); @@ -64,14 +63,10 @@ export default class SimpleAddUI extends UIElement { State.state.layerControlIsOpened.setData(true); }) - this._component = new ScrollableFullScreen( - Translations.t.general.add.title, - this.CreateContent(), - onClose - ); } InnerRender(): string { + this._component = this.CreateContent(); return this._component.Render(); } @@ -109,7 +104,7 @@ export default class SimpleAddUI extends UIElement { } const presetButtons = this.CreatePresetButtons() - return new Combine(presetButtons).SetClass("add-popup-all-buttons") + return new Combine(presetButtons) } diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 4524d34..7aaa468 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -16,74 +16,67 @@ export default class ShowDataLayer { private readonly _layerDict; private readonly _leafletMap: UIEventSource; - private readonly _onSelectedTrigger: any = {}; // osmId+geometry.type+matching_layer_id --> () => void constructor(features: UIEventSource<{ feature: any, freshness: Date }[]>, leafletMap: UIEventSource, - layoutToUse: LayoutConfig) { + layoutToUse: UIEventSource) { this._leafletMap = leafletMap; const self = this; const mp = leafletMap.data; this._layerDict = {}; - for (const layer of layoutToUse.layers) { - this._layerDict[layer.id] = layer; - } - - function openSelectedElementFeature(feature: any) { - if (feature === undefined) { - return; + layoutToUse.addCallbackAndRun(layoutToUse => { + for (const layer of layoutToUse.layers) { + this._layerDict[layer.id] = layer; } - const id = feature.properties.id + feature.geometry.type + feature._matching_layer_id; - const action = self._onSelectedTrigger[id]; - if (action) { - action(); - } - } + }); - - const knownFeatureIds = new Set(); - const geoLayer = self.CreateGeojsonLayer(); - mp.addLayer(geoLayer); + let geoLayer = undefined; let cluster = undefined; function update() { + console.log("Updating the data layer...", features.data.length, "objects loaded") if (features.data === undefined) { return; } if (leafletMap.data === undefined) { return; } - - const feats = features.data.map(ff => ff.feature); - for (const feat of feats) { + // clean all the old stuff away, if any + if (geoLayer !== undefined) { + mp.removeLayer(geoLayer); + } + if (cluster !== undefined) { + mp.removeLayer(cluster); + } + + const allFeats = features.data.map(ff => ff.feature); + console.log("AllFeats contain ", allFeats.length) + geoLayer = self.CreateGeojsonLayer(); + let i = 0; + for (const feat of allFeats) { const key = feat.geometry.type + feat.properties.id + feat.layer; - if (knownFeatureIds.has(key)) { - continue; - } - knownFeatureIds.add(key); // @ts-ignore geoLayer.addData(feat); - console.log("Added ", feat) + i++; } - if (cluster === undefined) { - if (layoutToUse.clustering.minNeededElements <= features.data.length) { - // Activate clustering if it wasn't already activated - const cl = window["L"]; // This is a dirty workaround, the clustering plugin binds to the L of the window, not of the namespace or something - cluster = cl.markerClusterGroup({disableClusteringAtZoom: layoutToUse.clustering.maxZoom}); - cluster.addLayer(geoLayer); - mp.removeLayer(geoLayer) - mp.addLayer(cluster); - } + if (layoutToUse.data.clustering.minNeededElements <= allFeats.length) { + // Activate clustering if it wasn't already activated + const cl = window["L"]; // This is a dirty workaround, the clustering plugin binds to the L of the window, not of the namespace or something + cluster = cl.markerClusterGroup({disableClusteringAtZoom: layoutToUse.data.clustering.maxZoom}); + cluster.addLayer(geoLayer); + mp.addLayer(cluster); + console.log("Added cluster", i) + } else { + mp.addLayer(geoLayer) + console.log("Added geoLayer", i) } } features.addCallback(() => update()); leafletMap.addCallback(() => update()); - - State.state.selectedElement.addCallbackAndRun(openSelectedElementFeature); update(); } @@ -103,6 +96,10 @@ export default class ShowDataLayer { const tagSource = State.state.allElements.addOrGetElement(feature) const layer: LayerConfig = this._layerDict[feature._matching_layer_id]; + if (layer === undefined) { + return; + } + const style = layer.GenerateLeafletStyle(tagSource, !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)); return L.marker(latLng, { icon: L.divIcon({ @@ -122,13 +119,13 @@ export default class ShowDataLayer { // No popup action defined -> Don't do anything return; } - const self = this; const popup = L.popup({ autoPan: true, closeOnEscapeKey: true, closeButton: false }, leafletLayer); + let isOpen = false; const tags = State.state.allElements.getEventSourceFor(feature); const uiElement = new LazyElement(() => @@ -141,30 +138,45 @@ export default class ShowDataLayer { "
Rendering
"); // By setting 90vh, leaflet will attempt to fit the entire screen and move the feature down popup.setContent(uiElement.Render()); popup.on('remove', () => { + if (!isOpen) { + return; + } + console.log("Closing popup...") + isOpen = false; ScrollableFullScreen.RestoreLeaflet(); // Just in case... State.state.selectedElement.setData(undefined); + }); leafletLayer.bindPopup(popup); // We first render the UIelement (which'll still need an update later on...) // But at least it'll be visible already + leafletLayer.on("popupopen", () => { - console.log("Popup opened") - uiElement.Activate(); + isOpen = true; State.state.selectedElement.setData(feature); + uiElement.Activate(); }) - const id = feature.properties.id + feature.geometry.type + feature._matching_layer_id; - this._onSelectedTrigger[id] - = () => { - if (!popup.isOpen()) { - console.log("Action triggered") - // Close all the popups which might still be opened - self._leafletMap.data.closePopup(); - leafletLayer.openPopup() - return; + + State.state.selectedElement.addCallbackAndRun(selected => { + if (selected === undefined) { + if (popup.isOpen() && isOpen) { + popup.remove(); + } + } else if (selected == feature && selected.geometry.type == feature.geometry.type) { + // If wayhandling introduces a centerpoint and an area, this code might become unstable: + // The popup for the centerpoint would open, a bit later the area would close the first popup and open it's own + // In the process, the 'selectedElement' is set to undefined and to the other feature again, causing an infinite loop + + // This is why we check for the geometry-type too + + if (!popup.isOpen() && !isOpen) { + isOpen = true; + leafletLayer.openPopup(); + } + } } - } - this._onSelectedTrigger[feature.properties.id.replace("/", "_")] = this._onSelectedTrigger[id]; + ); } diff --git a/index.css b/index.css index b5619ff..34fd1a7 100644 --- a/index.css +++ b/index.css @@ -359,15 +359,6 @@ a { color: var(--foreground-color); } - -.add-popup-all-buttons { - max-height: 50vh; - display: inline-block; - overflow-y: auto; - width: 100%; -} - - /**************************************/