From 3f29632cf4074c93889144a86a41697beaa72ee5 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 7 May 2021 14:36:12 +0200 Subject: [PATCH] Merge files from develop branch --- InitUiElements.ts | 18 +- Logic/Actors/SelectedFeatureHandler.ts | 39 +++- Logic/ElementStorage.ts | 1 + Logic/FeatureSource/FeaturePipeline.ts | 28 ++- Logic/FeatureSource/FeatureSourceMerger.ts | 43 +++- Logic/FeatureSource/FilteringFeatureSource.ts | 29 ++- Logic/FeatureSource/GeoJsonSource.ts | 106 +++++---- Logic/FeatureSource/OsmApiFeatureSource.ts | 91 ++++++++ Logic/MetaTagging.ts | 2 - Logic/Osm/OsmObject.ts | 208 +++++++++++++++--- Logic/SimpleMetaTagger.ts | 1 - Models/Constants.ts | 2 +- State.ts | 7 +- UI/BigComponents/MoreScreen.ts | 9 +- UI/ShowDataLayer.ts | 3 +- Utils.ts | 32 ++- 16 files changed, 484 insertions(+), 135 deletions(-) create mode 100644 Logic/FeatureSource/OsmApiFeatureSource.ts diff --git a/InitUiElements.ts b/InitUiElements.ts index 3c1ce6e85..23b8cd4f8 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -185,6 +185,9 @@ export class InitUiElements { // Reset the loading message once things are loaded new CenterMessageBox().AttachTo("centermessage"); + // At last, zoom to the needed location if the focus is on an element + + } static LoadLayoutFromHash(userLayoutParam: UIEventSource) { @@ -391,12 +394,21 @@ export class InitUiElements { const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap); State.state.layerUpdater = updater; - const source = new FeaturePipeline(state.filteredLayers, updater, state.layoutToUse, state.changes, state.locationControl); + + + + const source = new FeaturePipeline(state.filteredLayers, + updater, + state.osmApiFeatureSource, + state.layoutToUse, + state.changes, + state.locationControl, + state.selectedElement); new ShowDataLayer(source.features, State.state.leafletMap, State.state.layoutToUse); - new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source); - + const selectedFeatureHandler = new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source, State.state.osmApiFeatureSource); + selectedFeatureHandler.zoomToSelectedFeature(State.state.locationControl); } diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts index cacf54a26..e74708811 100644 --- a/Logic/Actors/SelectedFeatureHandler.ts +++ b/Logic/Actors/SelectedFeatureHandler.ts @@ -1,8 +1,12 @@ import {UIEventSource} from "../UIEventSource"; import FeatureSource from "../FeatureSource/FeatureSource"; +import {OsmObject, OsmObjectMeta} from "../Osm/OsmObject"; +import Loc from "../../Models/Loc"; +import FeaturePipeline from "../FeatureSource/FeaturePipeline"; +import OsmApiFeatureSource from "../FeatureSource/OsmApiFeatureSource"; /** - * Makes sure the hash shows the selected element and vice-versa + * Makes sure the hash shows the selected element and vice-versa. */ export default class SelectedFeatureHandler { private readonly _featureSource: FeatureSource; @@ -10,13 +14,16 @@ export default class SelectedFeatureHandler { private readonly _selectedFeature: UIEventSource; private static readonly _no_trigger_on = ["welcome","copyright","layers"] + private readonly _osmApiSource: OsmApiFeatureSource; constructor(hash: UIEventSource, selectedFeature: UIEventSource, - featureSource: FeatureSource) { + featureSource: FeaturePipeline, + osmApiSource: OsmApiFeatureSource) { this._hash = hash; this._selectedFeature = selectedFeature; this._featureSource = featureSource; + this._osmApiSource = osmApiSource; const self = this; hash.addCallback(h => { if (h === undefined || h === "") { @@ -26,6 +33,8 @@ export default class SelectedFeatureHandler { } }) + hash.addCallbackAndRun(h => self.downloadFeature(h)) + featureSource.features.addCallback(_ => self.selectFeature()); selectedFeature.addCallback(feature => { @@ -45,6 +54,32 @@ export default class SelectedFeatureHandler { } + // 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.indexOf(hash) >= 0){ + return; // No valid feature selected + } + // We should have a valid osm-ID and zoom to it + OsmObject.DownloadObject(hash, (element: OsmObject, meta: OsmObjectMeta) => { + 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(); + }) + } + + private downloadFeature(hash: string){ + if(hash === undefined || hash === ""){ + return; + } + if(SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){ + return; + } + this._osmApiSource.load(hash) + } + private selectFeature(){ const features = this._featureSource?.features?.data; if(features === undefined){ diff --git a/Logic/ElementStorage.ts b/Logic/ElementStorage.ts index 88e4ab151..8d518e42e 100644 --- a/Logic/ElementStorage.ts +++ b/Logic/ElementStorage.ts @@ -48,6 +48,7 @@ export class ElementStorage { const keptKeys = es.data; // The element already exists // We use the new feature to overwrite all the properties in the already existing eventsource + console.log("Merging multiple instances of ", elementId) let somethingChanged = false; for (const k in newProperties) { const v = newProperties[k]; diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index bae68a8bc..4eb94248a 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -18,27 +18,32 @@ export default class FeaturePipeline implements FeatureSource { public features: UIEventSource<{ feature: any; freshness: Date }[]>; - public readonly name = "FeaturePipeline" - + public readonly name = "FeaturePipeline" + constructor(flayers: UIEventSource<{ isDisplayed: UIEventSource, layerDef: LayerConfig }[]>, updater: FeatureSource, + fromOsmApi: FeatureSource, layout: UIEventSource, newPoints: FeatureSource, - locationControl: UIEventSource) { + locationControl: UIEventSource, + selectedElement: UIEventSource) { + + // first we metatag, then we save to get the metatags into storage too + // Note that we need to register before we do metatagging (as it expects the event sources) const amendedOverpassSource = new RememberingSource( new LocalStorageSaver( - new MetaTaggingFeatureSource( // first we metatag, then we save to get the metatags into storage too - new RegisteringFeatureSource( - new FeatureDuplicatorPerLayer(flayers, + new FeatureDuplicatorPerLayer(flayers, + new MetaTaggingFeatureSource( + new RegisteringFeatureSource( updater) )), layout)); const geojsonSources: FeatureSource [] = GeoJsonSource .ConstructMultiSource(flayers.data, locationControl) - .map(geojsonSource => new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource))); - + .map(geojsonSource => new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource))); + const amendedLocalStorageSource = new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout)) )); @@ -46,10 +51,16 @@ export default class FeaturePipeline implements FeatureSource { newPoints = new MetaTaggingFeatureSource(new FeatureDuplicatorPerLayer(flayers, new RegisteringFeatureSource(newPoints))); + const amendedOsmApiSource = new RememberingSource( + new FeatureDuplicatorPerLayer(flayers, + new MetaTaggingFeatureSource( + new RegisteringFeatureSource(fromOsmApi)))); + const merged = new FeatureSourceMerger([ amendedOverpassSource, + amendedOsmApiSource, amendedLocalStorageSource, newPoints, ...geojsonSources @@ -60,6 +71,7 @@ export default class FeaturePipeline implements FeatureSource { new FilteringFeatureSource( flayers, locationControl, + selectedElement, merged )); this.features = source.features; diff --git a/Logic/FeatureSource/FeatureSourceMerger.ts b/Logic/FeatureSource/FeatureSourceMerger.ts index 76c4c5211..9ceef4429 100644 --- a/Logic/FeatureSource/FeatureSourceMerger.ts +++ b/Logic/FeatureSource/FeatureSourceMerger.ts @@ -21,27 +21,48 @@ export default class FeatureSourceMerger implements FeatureSource { } private Update() { - let all = {}; // Mapping 'id' -> {feature, freshness} + + let somethingChanged = false; + const all: Map = new Map(); + // We seed the dictionary with the previously loaded features + const oldValues = this.features.data ?? []; + for (const oldValue of oldValues) { + all.set(oldValue.feature.id, oldValue) + } + for (const source of this._sources) { if (source?.features?.data === undefined) { continue; } for (const f of source.features.data) { const id = f.feature.properties.id; - const oldV = all[id]; - if (oldV === undefined) { - all[id] = f; - } else { - if (oldV.freshness < f.freshness) { - all[id] = f; - } + if (!all.has(id)) { + // This is a new feature + somethingChanged = true; + all.set(id, f); + continue; + } + + // This value has been seen already, either in a previous run or by a previous datasource + // Let's figure out if something changed + const oldV = all.get(id); + if (oldV.freshness < f.freshness) { + // Jup, this feature is fresher + all.set(id, f); + somethingChanged = true; } } } - const newList = []; - for (const id in all) { - newList.push(all[id]); + + if(!somethingChanged){ + // We don't bother triggering an update + return; } + + const newList = []; + all.forEach((value, key) => { + newList.push(value) + }) this.features.setData(newList); } diff --git a/Logic/FeatureSource/FilteringFeatureSource.ts b/Logic/FeatureSource/FilteringFeatureSource.ts index 74dcf59c7..e4c8b0f34 100644 --- a/Logic/FeatureSource/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/FilteringFeatureSource.ts @@ -5,12 +5,14 @@ import Loc from "../../Models/Loc"; export default class FilteringFeatureSource implements FeatureSource { public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); -public readonly name = "FilteringFeatureSource" + public readonly name = "FilteringFeatureSource" + constructor(layers: UIEventSource<{ isDisplayed: UIEventSource, layerDef: LayerConfig }[]>, location: UIEventSource, + selectedElement: UIEventSource, upstream: FeatureSource) { const self = this; @@ -18,7 +20,7 @@ public readonly name = "FilteringFeatureSource" function update() { const layerDict = {}; - if(layers.data.length == 0){ + if (layers.data.length == 0) { throw "No layers defined!" } for (const layer of layers.data) { @@ -32,6 +34,11 @@ public readonly name = "FilteringFeatureSource" const newFeatures = features.filter(f => { const layerId = f.feature._matching_layer_id; + if(selectedElement.data !== undefined && selectedElement.data?.id === f.feature.id){ + // This is the selected object - it gets a free pass even if zoom is not sufficient + return true; + } + if (layerId !== undefined) { const layer: { isDisplayed: UIEventSource, @@ -41,16 +48,16 @@ public readonly name = "FilteringFeatureSource" missingLayers.add(layerId) return true; } - + const isShown = layer.layerDef.isShown const tags = f.feature.properties; - if(isShown.IsKnown(tags)){ + if (isShown.IsKnown(tags)) { const result = layer.layerDef.isShown.GetRenderValue(f.feature.properties).txt; - if(result !== "yes"){ + if (result !== "yes") { return false; } } - + if (FilteringFeatureSource.showLayer(layer, location)) { return true; } @@ -69,7 +76,7 @@ public readonly name = "FilteringFeatureSource" }); console.log("Filtering layer source: input: ", upstream.features.data?.length, "output:", newFeatures.length) self.features.setData(newFeatures); - if(missingLayers.size > 0){ + if (missingLayers.size > 0) { console.error("Some layers were not found: ", Array.from(missingLayers)) } } @@ -86,7 +93,7 @@ public readonly name = "FilteringFeatureSource" if (l.zoom < layer.layerDef.minzoom) { continue; } - if(l.zoom > layer.layerDef.maxzoom){ + if (l.zoom > layer.layerDef.maxzoom) { continue; } if (!layer.isDisplayed.data) { @@ -98,13 +105,13 @@ public readonly name = "FilteringFeatureSource" }).addCallback(() => { update(); }); - + layers.addCallback(update); - + const registered = new Set>(); layers.addCallbackAndRun(layers => { for (const layer of layers) { - if(registered.has(layer.isDisplayed)){ + if (registered.has(layer.isDisplayed)) { continue; } registered.add(layer.isDisplayed); diff --git a/Logic/FeatureSource/GeoJsonSource.ts b/Logic/FeatureSource/GeoJsonSource.ts index ce329b0a1..beff2c801 100644 --- a/Logic/FeatureSource/GeoJsonSource.ts +++ b/Logic/FeatureSource/GeoJsonSource.ts @@ -20,9 +20,9 @@ export default class GeoJsonSource implements FeatureSource { private readonly layerId: string; private readonly seenids: Set = new Set() - private constructor(locationControl: UIEventSource, - flayer: { isDisplayed: UIEventSource, layerDef: LayerConfig }, - onFail?: ((errorMsg: any) => void)) { + private constructor(locationControl: UIEventSource, + flayer: { isDisplayed: UIEventSource, layerDef: LayerConfig }, + onFail?: ((errorMsg: any) => void)) { this.layerId = flayer.layerDef.id; let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); this.name = "GeoJsonSource of " + url; @@ -33,7 +33,7 @@ export default class GeoJsonSource implements FeatureSource { if (zoomLevel === undefined) { // This is a classic, static geojson layer if (onFail === undefined) { - onFail = errorMsg => { + onFail = _ => { } } this.onFail = onFail; @@ -43,57 +43,6 @@ export default class GeoJsonSource implements FeatureSource { this.ConfigureDynamicLayer(url, zoomLevel, locationControl, flayer) } } - - private ConfigureDynamicLayer(url: string, zoomLevel: number, locationControl: UIEventSource, flayer: { isDisplayed: UIEventSource, layerDef: LayerConfig }){ - // This is a dynamic template with a fixed zoom level - url = url.replace("{z}", "" + zoomLevel) - const loadedTiles = new Set(); - const self = this; - this.onFail = (msg, url) => { - console.warn(`Could not load geojson layer from`, url, "due to", msg) - loadedTiles.add(url); // We add the url to the 'loadedTiles' in order to not reload it in the future - } - - const neededTiles = locationControl.map( - location => { - // Yup, this is cheating to just get the bounds here - const bounds = State.state.leafletMap.data.getBounds() - const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) - const needed = new Set(); - for (let x = tileRange.xstart; x <= tileRange.xend; x++) { - for (let y = tileRange.ystart; y <= tileRange.yend; y++) { - let neededUrl = url.replace("{x}", "" + x).replace("{y}", "" + y); - needed.add(neededUrl) - } - } - return needed; - } - , [flayer.isDisplayed]); - neededTiles.stabilized(250).addCallback((needed: Set) => { - if (needed === undefined) { - return; - } - if (!flayer.isDisplayed.data) { - // No need to download! - the layer is disabled - return; - } - - if(locationControl.data.zoom < flayer.layerDef.minzoom){ - return; - } - - needed.forEach(neededTile => { - if (loadedTiles.has(neededTile)) { - return; - } - - loadedTiles.add(neededTile) - self.LoadJSONFrom(neededTile) - - }) - }) - - } /** * Merges together the layers which have the same source @@ -152,6 +101,53 @@ export default class GeoJsonSource implements FeatureSource { } + private ConfigureDynamicLayer(url: string, zoomLevel: number, locationControl: UIEventSource, flayer: { isDisplayed: UIEventSource, layerDef: LayerConfig }) { + // This is a dynamic template with a fixed zoom level + url = url.replace("{z}", "" + zoomLevel) + const loadedTiles = new Set(); + const self = this; + this.onFail = (msg, url) => { + console.warn(`Could not load geojson layer from`, url, "due to", msg) + loadedTiles.add(url); // We add the url to the 'loadedTiles' in order to not reload it in the future + } + + const neededTiles = locationControl.map( + _ => { + // Yup, this is cheating to just get the bounds here + const bounds = State.state.leafletMap.data.getBounds() + const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) + const needed = Utils.MapRange(tileRange, (x, y) => { + return url.replace("{x}", "" + x).replace("{y}", "" + y); + }) + return new Set(needed); + } + , [flayer.isDisplayed]); + neededTiles.stabilized(250).addCallback((needed: Set) => { + if (needed === undefined) { + return; + } + if (!flayer.isDisplayed.data) { + // No need to download! - the layer is disabled + return; + } + + if (locationControl.data.zoom < flayer.layerDef.minzoom) { + return; + } + + needed.forEach(neededTile => { + if (loadedTiles.has(neededTile)) { + return; + } + + loadedTiles.add(neededTile) + self.LoadJSONFrom(neededTile) + + }) + }) + + } + private LoadJSONFrom(url: string) { const eventSource = this.features; const self = this; diff --git a/Logic/FeatureSource/OsmApiFeatureSource.ts b/Logic/FeatureSource/OsmApiFeatureSource.ts new file mode 100644 index 000000000..13d2e4d82 --- /dev/null +++ b/Logic/FeatureSource/OsmApiFeatureSource.ts @@ -0,0 +1,91 @@ +import FeatureSource from "./FeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import {OsmObject} from "../Osm/OsmObject"; +import State from "../../State"; +import {Utils} from "../../Utils"; +import Loc from "../../Models/Loc"; + + +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(); + + constructor(location: UIEventSource) { + /* const self = this + location.addCallback(_ => { + self.loadArea() + }) + */ + + } + + + public load(id: string) { + OsmObject.DownloadObject(id, (element, meta) => { + const geojson = element.asGeoJson(); + geojson.id = geojson.properties.id; + this.features.setData([{feature: geojson, freshness: meta["_last_edit:timestamp"]}]) + }) + } + + /** + * Loads the current inview-area + */ + public loadArea(z: number = 16): boolean { + const layers = State.state.filteredLayers.data; + + const disabledLayers = layers.filter(layer => layer.layerDef.source.overpassScript !== undefined || layer.layerDef.source.geojsonSource !== undefined) + if (disabledLayers.length > 0) { + return false; + } + const loc = State.state.locationControl.data; + if (loc.zoom < 16) { + return false; + } + if (State.state.leafletMap.data === undefined) { + return false; // Not yet inited + } + const bounds = State.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/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index 0f1bd7199..ecce20409 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -1,9 +1,7 @@ import LayerConfig from "../Customizations/JSON/LayerConfig"; import SimpleMetaTagger from "./SimpleMetaTagger"; import {ExtraFunction} from "./ExtraFunction"; -import State from "../State"; import {Relation} from "./Osm/ExtractRelations"; -import {meta} from "@turf/turf"; interface Params { diff --git a/Logic/Osm/OsmObject.ts b/Logic/Osm/OsmObject.ts index b1f6d2024..5de7fd591 100644 --- a/Logic/Osm/OsmObject.ts +++ b/Logic/Osm/OsmObject.ts @@ -9,10 +9,14 @@ export abstract class OsmObject { tags: {} = {}; version: number; public changed: boolean = false; + timestamp: Date; protected constructor(type: string, id: number) { this.id = id; this.type = type; + this.tags = { + id: id + } } static DownloadObject(id, continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) { @@ -20,8 +24,7 @@ export abstract class OsmObject { const type = splitted[0]; const idN = splitted[1]; - const newContinuation = (element: OsmObject, meta :OsmObjectMeta) => { - console.log("Received: ", element, "with meta", meta); + const newContinuation = (element: OsmObject, meta: OsmObjectMeta) => { continuation(element, meta); } @@ -36,6 +39,80 @@ export abstract class OsmObject { } } + public static DownloadHistory(id: string, continuation: (versions: OsmObject[]) => void): void { + const splitted = id.split("/"); + const type = splitted[0]; + const idN = splitted[1]; + $.getJSON("https://openStreetMap.org/api/0.6/" + type + "/" + idN + "/history", data => { + const elements: any[] = data.elements; + const osmObjects: OsmObject[] = [] + for (const element of elements) { + let osmObject: OsmObject = null + switch (type) { + case("node"): + osmObject = new OsmNode(idN); + break; + case("way"): + osmObject = new OsmWay(idN); + break; + case("relation"): + osmObject = new OsmRelation(idN); + break; + } + osmObject?.LoadData(element); + osmObject?.SaveExtraData(element, []); + osmObjects.push(osmObject) + } + continuation(osmObjects) + }) + } + + private static ParseObjects(elements: any[]) : OsmObject[]{ + const objects: OsmObject[] = []; + const allNodes: Map = new Map() + for (const element of elements) { + const type = element.type; + const idN = element.id; + let osmObject: OsmObject = null + switch (type) { + case("node"): + const node = new OsmNode(idN); + allNodes.set(idN, node); + osmObject = node + node.SaveExtraData(element); + break; + case("way"): + osmObject = new OsmWay(idN); + const nodes = element.nodes.map(i => allNodes.get(i)); + osmObject.SaveExtraData(element, nodes) + break; + case("relation"): + osmObject = new OsmRelation(idN); + osmObject.SaveExtraData(element, []) + break; + } + osmObject.LoadData(element) + objects.push(osmObject) + } + return objects; + } + + //Loads an area from the OSM-api. + // bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds) + public static LoadArea(bounds: [[number, number], [number, number]], callback: (objects: OsmObject[]) => void) { + const minlon = bounds[0][1] + const maxlon = bounds[1][1] + const minlat = bounds[1][0] + const maxlat = bounds[0][0]; + const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}` + $.getJSON(url, data => { + const elements: any[] = data.elements; + const objects = OsmObject.ParseObjects(elements) + callback(objects); + + }) + } + public static DownloadAll(neededIds, knownElements: any = {}, continuation: ((knownObjects: any) => void)) { // local function which downloads all the objects one by one // this is one big loop, running one download, then rerunning the entire function @@ -50,7 +127,6 @@ export abstract class OsmObject { return; } - console.log("Downloading ", neededId); OsmObject.DownloadObject(neededId, function (element) { knownElements[neededId] = element; // assign the element for later, continue downloading the next element @@ -59,7 +135,12 @@ export abstract class OsmObject { ); } - abstract SaveExtraData(element); + // The centerpoint of the feature, as [lat, lon] + public abstract centerpoint(): [number, number]; + + public abstract asGeoJson(): any; + + abstract SaveExtraData(element: any, allElements: any[]); /** * Generates the changeset-XML for tags @@ -78,12 +159,20 @@ export abstract class OsmObject { Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) { const self = this; - $.getJSON("https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id, - function (data) { - const element = data.elements[0]; - self.tags = element.tags; - self.version = element.version; - self.SaveExtraData(element); + const full = this.type !== "way" ? "" : "/full"; + const url = "https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id + full; + $.getJSON(url, function (data) { + + const element = data.elements.pop(); + + let nodes = [] + if(data.elements.length > 2){ + nodes = OsmObject.ParseObjects(data.elements) + } + + self.LoadData(element) + self.SaveExtraData(element, nodes); + continuation(self, { "_last_edit:contributor": element.user, "_last_edit:contributor:uid": element.uid, @@ -102,7 +191,7 @@ export abstract class OsmObject { if (oldV == v) { return; } - console.log("WARNING: overwriting ", oldV, " with ", v, " for key ", k) + console.log("Overwriting ", oldV, " with ", v, " for key ", k) } this.tags[k] = v; if (v === undefined || v === "") { @@ -119,6 +208,25 @@ export abstract class OsmObject { } return 'version="' + this.version + '"'; } + + private LoadData(element: any): void { + this.tags = element.tags ?? this.tags; + this.version = element.version; + this.timestamp = element.timestamp; + const tgs = this.tags; + if(element.tags === undefined){ + // Simple node which is part of a way - not important + return; + } + tgs["_last_edit:contributor"] = element.user + tgs["_last_edit:contributor:uid"] = element.uid + tgs["_last_edit:changeset"] = element.changeset + tgs["_last_edit:timestamp"] = element.timestamp + tgs["_version_number"] = element.version + tgs["id"] = this.type + "/" + this.id; + + + } } @@ -135,21 +243,36 @@ export class OsmNode extends OsmObject { ChangesetXML(changesetId: string): string { let tags = this.TagsXML(); - let change = - ' \n' + + return ' \n' + tags + ' \n'; - - return change; } SaveExtraData(element) { this.lat = element.lat; this.lon = element.lon; } + + centerpoint(): [number, number] { + return [this.lat, this.lon]; + } + + asGeoJson() { + return { + "type": "Feature", + "properties": this.tags, + "geometry": { + "type": "Point", + "coordinates": [ + this.lon, + this.lat + ] + } + } + } } -export interface OsmObjectMeta{ +export interface OsmObjectMeta { "_last_edit:contributor": string, "_last_edit:contributor:uid": number, "_last_edit:changeset": number, @@ -161,12 +284,19 @@ export interface OsmObjectMeta{ export class OsmWay extends OsmObject { nodes: number[]; + coordinates: [number, number][] = [] + lat: number; + lon: number; constructor(id) { super("way", id); } + centerpoint(): [number, number] { + return [this.lat, this.lon]; + } + ChangesetXML(changesetId: string): string { let tags = this.TagsXML(); let nds = ""; @@ -174,18 +304,39 @@ export class OsmWay extends OsmObject { nds += ' \n'; } - let change = - ' \n' + + return ' \n' + nds + tags + ' \n'; - - return change; } - SaveExtraData(element) { + SaveExtraData(element, allNodes: OsmNode[]) { + + let latSum = 0 + let lonSum = 0 + + for (const node of allNodes) { + const cp = node.centerpoint(); + this.coordinates.push(cp); + latSum = cp[0] + lonSum = cp[1] + } + let count = this.coordinates.length; + this.lat = latSum / count; + this.lon = lonSum / count; this.nodes = element.nodes; } + + asGeoJson() { + return { + "type": "Feature", + "properties": this.tags, + "geometry": { + "type": "LineString", + "coordinates": this.coordinates.map(c => [c[1], c[0]]) + } + } + } } export class OsmRelation extends OsmObject { @@ -197,24 +348,29 @@ export class OsmRelation extends OsmObject { } + centerpoint(): [number, number] { + return [0, 0]; // TODO + } + ChangesetXML(changesetId: string): string { let members = ""; - for (const memberI in this.members) { - const member = this.members[memberI]; + for (const member of this.members) { members += ' \n'; } let tags = this.TagsXML(); - let change = - ' \n' + + return ' \n' + members + tags + ' \n'; - return change; } SaveExtraData(element) { this.members = element.members; } + + asGeoJson() { + throw "Not Implemented" + } } \ No newline at end of file diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index f807689bf..c1f942550 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -7,7 +7,6 @@ import {Utils} from "../Utils"; import opening_hours from "opening_hours"; import {UIElement} from "../UI/UIElement"; import Combine from "../UI/Base/Combine"; -import UpdateTagsFromOsmAPI from "./Actors/UpdateTagsFromOsmAPI"; const cardinalDirections = { diff --git a/Models/Constants.ts b/Models/Constants.ts index 0234c1270..f1e8195c1 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import { Utils } from "../Utils"; export default class Constants { - public static vNumber = "0.7.0c"; + public static vNumber = "0.7.1b"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { diff --git a/State.ts b/State.ts index 9da68bd13..96dbacbed 100644 --- a/State.ts +++ b/State.ts @@ -18,7 +18,7 @@ import LayerConfig from "./Customizations/JSON/LayerConfig"; import TitleHandler from "./Logic/Actors/TitleHandler"; import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; import {Relation} from "./Logic/Osm/ExtractRelations"; -import UpdateTagsFromOsmAPI from "./Logic/Actors/UpdateTagsFromOsmAPI"; +import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; /** * Contains the global state: a bunch of UI-event sources @@ -58,6 +58,8 @@ export default class State { public favouriteLayers: UIEventSource; public layerUpdater: UpdateFromOverpass; + + public osmApiFeatureSource : OsmApiFeatureSource ; public filteredLayers: UIEventSource<{ @@ -218,6 +220,7 @@ export default class State { this.allElements = new ElementStorage(); this.changes = new Changes(); + this.osmApiFeatureSource = new OsmApiFeatureSource(this.locationControl) new PendingChangesUploader(this.changes, this.selectedElement); @@ -253,8 +256,6 @@ export default class State { new TitleHandler(this.layoutToUse, this.selectedElement, this.allElements); - new UpdateTagsFromOsmAPI(this.selectedElement.map(el => el?.properties?.id), this.allElements) - } private static asFloat(source: UIEventSource): UIEventSource { diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index e5b036172..a19f20f0e 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -1,4 +1,3 @@ -import {VerticalCombine} from "../Base/VerticalCombine"; import {UIElement} from "../UIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; @@ -53,15 +52,17 @@ export default class MoreScreen extends UIElement { if(path === ""){ path = "." } + + const params = `z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}` let linkText = - `${path}/${layout.id.toLowerCase()}?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` + `${path}/${layout.id.toLowerCase()}?${params}` if (location.hostname === "localhost" || location.hostname === "127.0.0.1") { - linkText = `${path}/index.html?layout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}` + linkText = `${path}/index.html?layout=${layout.id}&${params}` } if (customThemeDefinition) { - linkText = `${path}/?userlayout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}` + linkText = `${path}/?userlayout=${layout.id}&${params}#${customThemeDefinition}` } diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 4ebe82e45..2eb7437f4 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -162,9 +162,8 @@ export default class ShowDataLayer { leafletLayer.on("popupopen", () => { State.state.selectedElement.setData(feature) }); - + this._popups.set(feature, leafletLayer); - } private CreateGeojsonLayer(): L.Layer { diff --git a/Utils.ts b/Utils.ts index aca8abc38..19a70b7b6 100644 --- a/Utils.ts +++ b/Utils.ts @@ -189,7 +189,7 @@ export class Utils { * @param z * @param x * @param y - * @returns [[lat, lon], [lat, lon]] + * @returns [[maxlat, minlon], [minlat, maxlon]] */ static tile_bounds(z: number, x: number, y: number): [[number, number], [number, number]] { return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]] @@ -201,8 +201,8 @@ export class Utils { static embedded_tile(lat: number, lon: number, z: number): { x: number, y: number, z: number } { return {x: Utils.lon2tile(lon, z), y: Utils.lat2tile(lat, z), z: z} } - - static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1:number, lon1: number) : TileRange{ + + static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1: number, lon1: number): TileRange { const t0 = Utils.embedded_tile(lat0, lon0, zoomlevel) const t1 = Utils.embedded_tile(lat1, lon1, zoomlevel) @@ -211,12 +211,12 @@ export class Utils { const ystart = Math.min(t0.y, t1.y) const yend = Math.max(t0.y, t1.y) const total = (1 + xend - xstart) * (1 + yend - ystart) - + return { xstart: xstart, xend: xend, ystart: ystart, - yend: yend, + yend: yend, total: total, zoomlevel: zoomlevel } @@ -274,8 +274,27 @@ export class Utils { private static lat2tile(lat, zoom) { return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom))); } -} + public static MapRange (tileRange: TileRange, f: (x: number, y: number) => T): T[] { + const result : T[] = [] + for (let x = tileRange.xstart; x <= tileRange.xend; x++) { + for (let y = tileRange.ystart; y <= tileRange.yend; y++) { + const t= f(x, y); + result.push(t) + } + } + return result; + } + + public static downloadTxtFile (contents: string, fileName: string = "download.txt") { + const element = document.createElement("a"); + const file = new Blob([contents], {type: 'text/plain'}); + element.href = URL.createObjectURL(file); + element.download = fileName; + document.body.appendChild(element); // Required for this to work in FireFox + element.click(); + } +} export interface TileRange{ xstart: number, @@ -284,4 +303,5 @@ export interface TileRange{ yend: number, total: number, zoomlevel: number + } \ No newline at end of file