diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts index 73e69d72c..b45e87b93 100644 --- a/Logic/Actors/SelectedFeatureHandler.ts +++ b/Logic/Actors/SelectedFeatureHandler.ts @@ -3,15 +3,20 @@ import {OsmObject} from "../Osm/OsmObject"; import Loc from "../../Models/Loc"; import {ElementStorage} from "../ElementStorage"; import FeaturePipeline from "../FeatureSource/FeaturePipeline"; +import {GeoOperations} from "../GeoOperations"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; /** * Makes sure the hash shows the selected element and vice-versa. */ export default class SelectedFeatureHandler { private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "", undefined]) - hash: UIEventSource; + private readonly hash: UIEventSource; private readonly state: { - selectedElement: UIEventSource + selectedElement: UIEventSource, + allElements: ElementStorage, + locationControl: UIEventSource, + layoutToUse: LayoutConfig } constructor( @@ -19,7 +24,9 @@ export default class SelectedFeatureHandler { state: { selectedElement: UIEventSource, allElements: ElementStorage, - featurePipeline: FeaturePipeline + featurePipeline: FeaturePipeline, + locationControl: UIEventSource, + layoutToUse: LayoutConfig } ) { this.hash = hash; @@ -27,30 +34,9 @@ export default class SelectedFeatureHandler { // If the hash changes, set the selected element correctly - function setSelectedElementFromHash(h){ - if (h === undefined || h === "") { - // Hash has been cleared - we clear the selected element - state.selectedElement.setData(undefined); - }else{ - // we search the element to select - const feature = state.allElements.ContainingFeatures.get(h) - if(feature === undefined){ - return; - } - const currentlySeleced = state.selectedElement.data - if(currentlySeleced === undefined){ - state.selectedElement.setData(feature) - return; - } - if(currentlySeleced.properties?.id === feature.properties.id){ - // We already have the right feature - return; - } - state.selectedElement.setData(feature) - } - } - hash.addCallback(setSelectedElementFromHash) + const self = this; + hash.addCallback(() => self.setSelectedElementFromHash()) // IF the selected element changes, set the hash correctly @@ -66,41 +52,103 @@ export default class SelectedFeatureHandler { hash.setData(h) } }) - - state.featurePipeline.newDataLoadedSignal.addCallbackAndRunD(_ => { + + state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD(_ => { // New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet - if(hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)){ + if (hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)) { // This is an invalid hash anyway return; } - if(state.selectedElement.data !== undefined){ + if (state.selectedElement.data !== undefined) { // We already have something selected return; } - setSelectedElementFromHash(hash.data) + self.setSelectedElementFromHash() + }) + + + this.initialLoad() + + } + + + /** + * On startup: check if the hash is loaded and eventually zoom to it + * @private + */ + private initialLoad() { + const hash = this.hash.data + if (hash === undefined || hash === "" || hash.indexOf("-") >= 0) { + return; + } + if (SelectedFeatureHandler._no_trigger_on.has(hash)) { + return; + } + + OsmObject.DownloadObjectAsync(hash).then(obj => { + + try { + + console.log("Downloaded selected object from OSM-API for initial load: ", hash) + const geojson = obj.asGeoJson() + this.state.allElements.addOrGetElement(geojson) + this.state.selectedElement.setData(geojson) + this.zoomToSelectedFeature() + } catch (e) { + console.error(e) + } + }) } - // If a feature is selected via the hash, zoom there - public zoomToSelectedFeature(location: UIEventSource) { - const hash = this.hash.data; - if (hash === undefined || SelectedFeatureHandler._no_trigger_on.has(hash)) { - return; // No valid feature selected - } - // We should have a valid osm-ID and zoom to it... But we wrap it in try-catch to be sure - try { - - OsmObject.DownloadObject(hash).addCallbackAndRunD(element => { - const centerpoint = element.centerpoint(); - console.log("Zooming to location for select point: ", centerpoint) - location.data.lat = centerpoint[0] - location.data.lon = centerpoint[1] - location.ping(); - }) - } catch (e) { - console.error("Could not download OSM-object with id", hash, " - probably a weird hash") + private setSelectedElementFromHash() { + const state = this.state + const h = this.hash.data + if (h === undefined || h === "") { + // Hash has been cleared - we clear the selected element + state.selectedElement.setData(undefined); + } else { + // we search the element to select + const feature = state.allElements.ContainingFeatures.get(h) + if (feature === undefined) { + return; + } + const currentlySeleced = state.selectedElement.data + if (currentlySeleced === undefined) { + state.selectedElement.setData(feature) + return; + } + if (currentlySeleced.properties?.id === feature.properties.id) { + // We already have the right feature + return; + } + state.selectedElement.setData(feature) } } + // If a feature is selected via the hash, zoom there + private zoomToSelectedFeature() { + + const selected = this.state.selectedElement.data + if(selected === undefined){ + return + } + + const centerpoint= GeoOperations.centerpointCoordinates(selected) + const location = this.state.locationControl; + location.data.lon = centerpoint[0] + location.data.lat = centerpoint[1] + + const minZoom = Math.max(14, ...(this.state.layoutToUse?.layers?.map(l => l.minzoomVisible) ?? [])) + if(location.data.zoom < minZoom ){ + location.data.zoom = minZoom + } + + location.ping(); + + + + } + } \ No newline at end of file diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index 5c7f1b9a6..c6416fe39 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -23,6 +23,10 @@ import ScrollableFullScreen from "./Base/ScrollableFullScreen"; import Translations from "./i18n/Translations"; import SimpleAddUI from "./BigComponents/SimpleAddUI"; import StrayClickHandler from "../Logic/Actors/StrayClickHandler"; +import Lazy from "./Base/Lazy"; +import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer"; +import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; +import FilteredLayer from "../Models/FilteredLayer"; export class DefaultGuiState { public readonly welcomeMessageIsOpened; @@ -81,12 +85,22 @@ export default class DefaultGUI { constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { this.state = state; this._guiState = guiState; - const self = this; if (state.layoutToUse.customCss !== undefined) { Utils.LoadCustomCss(state.layoutToUse.customCss); } + + this.SetupUIElements(); + this.SetupMap() + + } + + + private SetupMap(){ + const state = this.state; + const guiState = this._guiState; + // Attach the map state.mainMapObject.SetClass("w-full h-full") .AttachTo("leafletDiv") @@ -96,8 +110,28 @@ export default class DefaultGUI { state ) - this.InitWelcomeMessage(); + new ShowDataLayer({ + leafletMap: state.leafletMap, + layerToShow: AllKnownLayers.sharedLayers.get("home_location"), + features: state.homeLocation, + enablePopups: false, + }) + + state.leafletMap.addCallbackAndRunD(_ => { + // Lets assume that all showDataLayers are initialized at this point + state.selectedElement.ping() + State.state.locationControl.ping(); + return true; + }) + + } + + private SetupUIElements(){ + const state = this.state; + const guiState = this._guiState; + + const self =this Toggle.If(state.featureSwitchUserbadge, () => new UserBadge(state) ).AttachTo("userbadge") @@ -119,36 +153,21 @@ export default class DefaultGUI { } - new Toggle(self.InitWelcomeMessage(), - Toggle.If(state.featureSwitchIframePopoutEnabled, iframePopout), + new Toggle(new Lazy(() => self.InitWelcomeMessage()), + Toggle.If(state.featureSwitchIframePopoutEnabled, iframePopout), state.featureSwitchWelcomeMessage ).AttachTo("messagesbox"); new LeftControls(state, guiState).AttachTo("bottom-left"); new RightControls(state).AttachTo("bottom-right"); - State.state.locationControl.ping(); + new CenterMessageBox(state).AttachTo("centermessage"); document .getElementById("centermessage") .classList.add("pointer-events-none"); - - - new ShowDataLayer({ - leafletMap: state.leafletMap, - layerToShow: AllKnownLayers.sharedLayers.get("home_location"), - features: state.homeLocation, - enablePopups: false, - }) - - state.leafletMap.addCallbackAndRunD(_ => { - // Lets assume that all showDataLayers are initialized at this point - state.selectedElement.ping() - return true; - }) - } - private InitWelcomeMessage() { + private InitWelcomeMessage() : BaseUIElement{ const isOpened = this._guiState.welcomeMessageIsOpened const fullOptions = new FullWelcomePaneWithTabs(isOpened, this._guiState.welcomeMessageOpenedTab, this.state); @@ -180,7 +199,7 @@ export default class DefaultGUI { public setupClickDialogOnMap(filterViewIsOpened: UIEventSource, state: FeaturePipelineState) { - function setup(){ + function setup() { let presetCount = 0; for (const layer of state.layoutToUse.layers) { for (const preset of layer.presets) { diff --git a/test/Actors.spec.ts b/test/Actors.spec.ts index a84768977..b53cfd5c7 100644 --- a/test/Actors.spec.ts +++ b/test/Actors.spec.ts @@ -5,11 +5,15 @@ import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdat import UserRelatedState from "../Logic/State/UserRelatedState"; import {Utils} from "../Utils"; import ScriptUtils from "../scripts/ScriptUtils"; +import SelectedFeatureHandler from "../Logic/Actors/SelectedFeatureHandler"; +import {UIEventSource} from "../Logic/UIEventSource"; +import {ElementStorage} from "../Logic/ElementStorage"; +import Loc from "../Models/Loc"; export default class ActorsSpec extends T { constructor() { - + const latestTags = { "amenity": "public_bookcase", "books": "children;adults", @@ -22,7 +26,7 @@ export default class ActorsSpec extends T { "operator": "Huisbewoner", "public_bookcase:type": "reading_box" } - + Utils.injectJsonDownloadForTests( "https://www.openstreetmap.org/api/0.6/node/5568693115", { @@ -92,7 +96,7 @@ export default class ActorsSpec extends T { // THis should trigger a download of the latest feaures and update the tags // However, this doesn't work with ts-node for some reason state.selectedElement.setData(feature) - + SelectedElementTagsUpdater.applyUpdate(state, latestTags, feature.properties.id) // The name should be updated @@ -100,7 +104,35 @@ export default class ActorsSpec extends T { // The fixme should be removed T.equals(undefined, feature.properties.fixme) - }] + }], + ["Hash without selected element should download geojson from OSM-API", async () => { + const hash = new UIEventSource("node/5568693115") + const selected = new UIEventSource(undefined) + const loc = new UIEventSource({ + lat: 0, + lon: 0, + zoom: 0 + }) + + + loc.addCallback(_ => { + T.equals("node/5568693115", selected.data.properties.id) + T.equals(14, loc.data.zoom) + T.equals( 51.2179199, loc.data.lat) + }) + + new SelectedFeatureHandler(hash, { + selectedElement: selected, + allElements: new ElementStorage(), + featurePipeline: undefined, + locationControl: loc, + layoutToUse: undefined + }) + + + + + }] ]);