diff --git a/InitUiElements.ts b/InitUiElements.ts index 353192dc3..3f591a3d1 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -35,6 +35,7 @@ import {LayoutConfigJson} from "./Models/ThemeConfig/Json/LayoutConfigJson"; import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; import LayerConfig from "./Models/ThemeConfig/LayerConfig"; import Minimap from "./UI/Base/Minimap"; +import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; export class InitUiElements { static InitAll( @@ -194,6 +195,8 @@ export class InitUiElements { State.state.locationControl.ping(); } + new SelectedFeatureHandler(Hash.hash, State.state) + // Reset the loading message once things are loaded new CenterMessageBox().AttachTo("centermessage"); document @@ -404,15 +407,6 @@ export class InitUiElements { }, state ); - /* const selectedFeatureHandler = new SelectedFeatureHandler( - Hash.hash, - State.state.selectedElement, - source, - State.state.osmApiFeatureSource - ); - selectedFeatureHandler.zoomToSelectedFeature( - State.state.locationControl - );*/ } private static setupAllLayerElements() { diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts index 07ff7f4a7..95a744df6 100644 --- a/Logic/Actors/SelectedFeatureHandler.ts +++ b/Logic/Actors/SelectedFeatureHandler.ts @@ -1,49 +1,45 @@ import {UIEventSource} from "../UIEventSource"; -import FeatureSource from "../FeatureSource/FeatureSource"; import {OsmObject} from "../Osm/OsmObject"; import Loc from "../../Models/Loc"; -import FeaturePipeline from "../FeatureSource/FeaturePipeline"; -import OsmApiFeatureSource from "../FeatureSource/Sources/OsmApiFeatureSource"; +import {ElementStorage} from "../ElementStorage"; /** * Makes sure the hash shows the selected element and vice-versa. */ export default class SelectedFeatureHandler { private static readonly _no_trigger_on = ["welcome", "copyright", "layers", "new"] - private readonly _featureSource: FeatureSource; - private readonly _hash: UIEventSource; - private readonly _selectedFeature: UIEventSource; - private readonly _osmApiSource: OsmApiFeatureSource; + hash: UIEventSource; + private readonly state: { + selectedElement: UIEventSource + } - constructor(hash: UIEventSource, - selectedFeature: UIEventSource, - featureSource: FeaturePipeline, - osmApiSource: OsmApiFeatureSource) { - this._hash = hash; - this._selectedFeature = selectedFeature; - this._featureSource = featureSource; - this._osmApiSource = osmApiSource; - const self = this; + constructor( + hash: UIEventSource, + state: { + selectedElement: UIEventSource, + allElements: ElementStorage; + } + ) { + this.hash = hash; + + this.state = state + + // Getting a blank hash clears the selected element hash.addCallback(h => { if (h === undefined || h === "") { - selectedFeature.setData(undefined); - } else { - self.selectFeature(); + state.selectedElement.setData(undefined); + }else{ + const feature = state.allElements.ContainingFeatures.get(h) + if(feature !== undefined){ + state.selectedElement.setData(feature) + } } }) - hash.addCallbackAndRunD(h => { - try { - self.downloadFeature(h) - } catch (e) { - console.error("Could not download feature, probably a weird hash") - } - }) - featureSource.features.addCallback(_ => self.selectFeature()); - - selectedFeature.addCallback(feature => { + state.selectedElement.addCallback(feature => { if (feature === undefined) { + console.trace("Resetting hash") if (SelectedFeatureHandler._no_trigger_on.indexOf(hash.data) < 0) { hash.setData("") } @@ -55,13 +51,11 @@ export default class SelectedFeatureHandler { } }) - this.selectFeature(); - } // If a feature is selected via the hash, zoom there public zoomToSelectedFeature(location: UIEventSource) { - const hash = this._hash.data; + const hash = this.hash.data; if (hash === undefined || SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0) { return; // No valid feature selected } @@ -80,42 +74,4 @@ export default class SelectedFeatureHandler { } } - private downloadFeature(hash: string) { - if (hash === undefined || hash === "") { - return; - } - if (SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0) { - return; - } - try { - - this._osmApiSource.load(hash) - } catch (e) { - console.log("Could not download feature, probably a weird hash:", hash) - } - } - - private selectFeature() { - const features = this._featureSource?.features?.data; - if (features === undefined) { - return; - } - if (this._selectedFeature.data?.properties?.id === this._hash.data) { - // Feature already selected - return; - } - - const hash = this._hash.data; - if (hash === undefined || hash === "" || hash === "#") { - return; - } - for (const feature of features) { - const id = feature.feature?.properties?.id; - if (id === hash) { - this._selectedFeature.setData(feature.feature); - break; - } - } - } - } \ No newline at end of file diff --git a/Logic/ContributorCount.ts b/Logic/ContributorCount.ts index 6a4c2da25..2dcd8450f 100644 --- a/Logic/ContributorCount.ts +++ b/Logic/ContributorCount.ts @@ -1,9 +1,7 @@ /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions -import FeatureSource from "./FeatureSource/FeatureSource"; import {UIEventSource} from "./UIEventSource"; import FeaturePipeline from "./FeatureSource/FeaturePipeline"; import Loc from "../Models/Loc"; -import State from "../State"; import {BBox} from "./GeoOperations"; export default class ContributorCount { @@ -33,7 +31,7 @@ export default class ContributorCount { if (this.lastUpdate !== undefined && ((now.getTime() - this.lastUpdate.getTime()) < 1000 * 60)) { return; } - console.log("Calculating contributors") + this.lastUpdate = now; const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox) const hist = new Map(); for (const list of featuresList) { diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index ec4a052be..80f1949f1 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -36,7 +36,6 @@ export default class FeaturePipeline implements FeatureSourceState { constructor( handleFeatureSource: (source: FeatureSourceForLayer) => void, state: { - osmApiFeatureSource: FeatureSource, filteredLayers: UIEventSource, locationControl: UIEventSource, selectedElement: UIEventSource, diff --git a/Logic/FeatureSource/Sources/OsmApiFeatureSource.ts b/Logic/FeatureSource/Sources/OsmApiFeatureSource.ts deleted file mode 100644 index c1c846602..000000000 --- a/Logic/FeatureSource/Sources/OsmApiFeatureSource.ts +++ /dev/null @@ -1,110 +0,0 @@ -import FeatureSource from "../FeatureSource"; -import {UIEventSource} from "../../UIEventSource"; -import Loc from "../../../Models/Loc"; -import FilteredLayer from "../../../Models/FilteredLayer"; -import {Utils} from "../../../Utils"; -import {OsmObject} from "../../Osm/OsmObject"; - - -export default class OsmApiFeatureSource implements FeatureSource { - public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); - public readonly name: string = "OsmApiFeatureSource"; - private readonly loadedTiles: Set = new Set(); - private readonly _state: { - leafletMap: UIEventSource; - locationControl: UIEventSource, filteredLayers: UIEventSource}; - - constructor(state: {locationControl: UIEventSource, filteredLayers: UIEventSource, leafletMap: UIEventSource, - overpassMaxZoom: UIEventSource}) { - this._state = state; - const self = this; - function update(){ - const minZoom = state.overpassMaxZoom.data; - const location = state.locationControl.data - if(minZoom === undefined || location === undefined){ - return; - } - if(minZoom < 14){ - throw "MinZoom should be at least 14 or higher, OSM-api won't work otherwise" - } - if(location.zoom > minZoom){ - return; - } - self.loadArea() - } - } - - - public load(id: string) { - if (id.indexOf("-") >= 0) { - // Newly added point - not yet in OSM - return; - } - console.debug("Downloading", id, "from the OSM-API") - OsmObject.DownloadObject(id).addCallbackAndRunD(element => { - try { - const geojson = element.asGeoJson(); - geojson.id = geojson.properties.id; - this.features.setData([{feature: geojson, freshness: element.timestamp}]) - } catch (e) { - console.error(e) - } - }) - } - - /** - * Loads the current inview-area - */ - public loadArea(z: number = 14): boolean { - const layers = this._state.filteredLayers.data; - - const disabledLayers = layers.filter(layer => layer.layerDef.source.overpassScript !== undefined || layer.layerDef.source.geojsonSource !== undefined) - if (disabledLayers.length > 0) { - return false; - } - if (this._state.leafletMap.data === undefined) { - return false; // Not yet inited - } - const bounds = this._state.leafletMap.data.getBounds() - const tileRange = Utils.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) - const self = this; - Utils.MapRange(tileRange, (x, y) => { - const key = x + "/" + y; - if (self.loadedTiles.has(key)) { - return; - } - - self.loadedTiles.add(key); - - const bounds = Utils.tile_bounds(z, x, y); - console.log("Loading OSM data tile", z, x, y, " with bounds", bounds) - OsmObject.LoadArea(bounds, objects => { - const keptGeoJson: { feature: any, freshness: Date }[] = [] - // Which layer does the object match? - for (const object of objects) { - - for (const flayer of layers) { - const layer = flayer.layerDef; - const tags = object.tags - const doesMatch = layer.source.osmTags.matchesProperties(tags); - if (doesMatch) { - const geoJson = object.asGeoJson(); - geoJson._matching_layer_id = layer.id - keptGeoJson.push({feature: geoJson, freshness: object.timestamp}) - break; - } - - } - - } - - self.features.setData(keptGeoJson) - }); - - }); - - return true; - - } - -} \ No newline at end of file diff --git a/State.ts b/State.ts index 8c08873cb..54af37fd4 100644 --- a/State.ts +++ b/State.ts @@ -13,7 +13,6 @@ import Loc from "./Models/Loc"; import Constants from "./Models/Constants"; import TitleHandler from "./Logic/Actors/TitleHandler"; import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; -import OsmApiFeatureSource from "./Logic/FeatureSource/Sources/OsmApiFeatureSource"; import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; import FilteredLayer from "./Models/FilteredLayer"; import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; @@ -55,8 +54,6 @@ export default class State { public favouriteLayers: UIEventSource; - public osmApiFeatureSource: OsmApiFeatureSource; - public filteredLayers: UIEventSource = new UIEventSource([], "filteredLayers"); /** @@ -395,8 +392,6 @@ export default class State { new ChangeToElementsActor(this.changes, this.allElements) - this.osmApiFeatureSource = new OsmApiFeatureSource(this) - new PendingChangesUploader(this.changes, this.selectedElement); this.mangroveIdentity = new MangroveIdentity( diff --git a/UI/ShowDataLayer/ShowDataLayer.ts b/UI/ShowDataLayer/ShowDataLayer.ts index 927a1008d..73a68247c 100644 --- a/UI/ShowDataLayer/ShowDataLayer.ts +++ b/UI/ShowDataLayer/ShowDataLayer.ts @@ -16,11 +16,20 @@ export default class ShowDataLayer { // Used to generate a fresh ID when needed private _cleanCount = 0; - - constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig}) { + private geoLayer = undefined; + + /** + * If the selected element triggers, this is used to lookup the correct layer and to open the popup + * Used to avoid a lot of callbacks on the selected element + * @private + */ + private readonly leafletLayersPerId = new Map() + + + constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) { this._leafletMap = options.leafletMap; this._enablePopups = options.enablePopups ?? true; - if(options.features === undefined){ + if (options.features === undefined) { throw "Invalid ShowDataLayer invocation" } const features = options.features.features.map(featFreshes => featFreshes.map(ff => ff.feature)); @@ -28,58 +37,77 @@ export default class ShowDataLayer { this._layerToShow = options.layerToShow; const self = this; - let geoLayer = undefined; + features.addCallback(() => self.update(options)); + options.leafletMap.addCallback(() => self.update(options)); + this.update(options); - function update() { - if (features.data === undefined) { + + State.state.selectedElement.addCallbackAndRunD(selected => { + if (self._leafletMap.data === undefined) { return; } - const mp =options. leafletMap.data; - - if (mp === undefined) { + const v = self.leafletLayersPerId.get(selected.properties.id) + if(v === undefined){return;} + const leafletLayer = v.leafletlayer + const feature = v.feature + if (leafletLayer.getPopup().isOpen()) { return; } - - self._cleanCount++ - // clean all the old stuff away, if any - if (geoLayer !== undefined) { - mp.removeLayer(geoLayer); - } - - const allFeats = features.data; - geoLayer = self.CreateGeojsonLayer(); - for (const feat of allFeats) { - if (feat === undefined) { - continue + if (selected.properties.id === feature.properties.id) { + // A small sanity check to prevent infinite loops: + if (selected.geometry.type === feature.geometry.type // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again + && feature.id === feature.properties.id // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too + ) { + leafletLayer.openPopup() } - // @ts-ignore - geoLayer.addData(feat); - } - - mp.addLayer(geoLayer) - - if (options.zoomToFeatures ?? false) { - try { - mp.fitBounds(geoLayer.getBounds(), {animate: false}) - } catch (e) { - console.error(e) + if (feature.id !== feature.properties.id) { + console.trace("Not opening the popup for", feature) } + } - if (self._enablePopups) { - State.state.selectedElement.ping() - } + }) + } + + private update(options) { + if (this._features.data === undefined) { + return; + } + const mp = options.leafletMap.data; + + if (mp === undefined) { + return; + } + this._cleanCount++ + // clean all the old stuff away, if any + if (this.geoLayer !== undefined) { + mp.removeLayer(this.geoLayer); } - features.addCallback(() => update()); - options.leafletMap.addCallback(() => update()); - update(); + const allFeats = this._features.data; + this.geoLayer = this.CreateGeojsonLayer(); + for (const feat of allFeats) { + if (feat === undefined) { + continue + } + this.geoLayer.addData(feat); + } + + mp.addLayer(this.geoLayer) + + if (options.zoomToFeatures ?? false) { + try { + mp.fitBounds(this.geoLayer.getBounds(), {animate: false}) + } catch (e) { + console.error(e) + } + } } private createStyleFor(feature) { const tagsSource = State.state.allElements.addOrGetElement(feature); // Every object is tied to exactly one layer - const layer = this._layerToShow + const layer = this._layerToShow return layer?.GenerateLeafletStyle(tagsSource, true); } @@ -88,7 +116,7 @@ export default class ShowDataLayer { // We have to convert them to the appropriate icon // Click handling is done in the next step - const layer: LayerConfig = this._layerToShow + const layer: LayerConfig = this._layerToShow if (layer === undefined) { return; } @@ -159,28 +187,9 @@ export default class ShowDataLayer { infobox.AttachTo(id) infobox.Activate(); }); - const self = this; - State.state.selectedElement.addCallbackAndRunD(selected => { - if (self._leafletMap.data === undefined) { - return; - } - if (leafletLayer.getPopup().isOpen()) { - return; - } - if (selected.properties.id === feature.properties.id) { - // A small sanity check to prevent infinite loops: - if (selected.geometry.type === feature.geometry.type // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again - && feature.id === feature.properties.id // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too - ) { - leafletLayer.openPopup() - } - if (feature.id !== feature.properties.id) { - console.trace("Not opening the popup for", feature) - } - - } - }) + // Add the feature to the index to open the popup when needed + this.leafletLayersPerId.set(feature.properties.id, {feature: feature, leafletlayer: leafletLayer}) } private CreateGeojsonLayer(): L.Layer {