diff --git a/InitUiElements.ts b/InitUiElements.ts index e3e61ce2e..353192dc3 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -1,7 +1,6 @@ import {FixedUiElement} from "./UI/Base/FixedUiElement"; import Toggle from "./UI/Input/Toggle"; import State from "./State"; -import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource"; import {UIEventSource} from "./Logic/UIEventSource"; import {QueryParameters} from "./Logic/Web/QueryParameters"; import StrayClickHandler from "./Logic/Actors/StrayClickHandler"; @@ -18,17 +17,15 @@ import * as L from "leaflet"; import Img from "./UI/Base/Img"; import UserDetails from "./Logic/Osm/OsmConnection"; import Attribution from "./UI/BigComponents/Attribution"; -import LayerResetter from "./Logic/Actors/LayerResetter"; +import BackgroundLayerResetter from "./Logic/Actors/BackgroundLayerResetter"; import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs"; -import ShowDataLayer from "./UI/ShowDataLayer"; +import ShowDataLayer from "./UI/ShowDataLayer/ShowDataLayer"; import Hash from "./Logic/Web/Hash"; import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; import ScrollableFullScreen from "./UI/Base/ScrollableFullScreen"; import Translations from "./UI/i18n/Translations"; import MapControlButton from "./UI/MapControlButton"; -import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler"; import LZString from "lz-string"; -import FeatureSource from "./Logic/FeatureSource/FeatureSource"; import AllKnownLayers from "./Customizations/AllKnownLayers"; import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; import {TagsFilter} from "./Logic/Tags/TagsFilter"; @@ -38,7 +35,6 @@ 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 Constants from "./Models/Constants"; export class InitUiElements { static InitAll( @@ -130,10 +126,9 @@ export class InitUiElements { } } if (somethingChanged) { - console.log("layoutToUse.layers:", layoutToUse.layers); State.state.layoutToUse.data.layers = Array.from(neededLayers); State.state.layoutToUse.ping(); - State.state.layerUpdater?.ForceRefresh(); + State.state.featurePipeline?.ForceRefresh(); } } @@ -320,7 +315,7 @@ export class InitUiElements { (layer) => layer.id ); - new LayerResetter( + new BackgroundLayerResetter( State.state.backgroundLayer, State.state.locationControl, State.state.availableBackgroundLayers, @@ -333,13 +328,14 @@ export class InitUiElements { State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, - State.state.leafletMap + State.state.currentBounds ); - new Minimap({ + Minimap.createMiniMap({ background: State.state.backgroundLayer, location: State.state.locationControl, leafletMap: State.state.leafletMap, + bounds: State.state.currentBounds, attribution: attr, lastClickLocation: State.state.LastClickLocation }).SetClass("w-full h-full") @@ -371,7 +367,7 @@ export class InitUiElements { } } - private static InitLayers(): FeatureSource { + private static InitLayers(): void { const state = State.state; state.filteredLayers = state.layoutToUse.map((layoutToUse) => { const flayers = []; @@ -396,51 +392,35 @@ export class InitUiElements { return flayers; }); - const updater = new LoadFromOverpass( - state.locationControl, - state.layoutToUse, - state.leafletMap, - state.overpassUrl, - state.overpassTimeout, - Constants.useOsmApiAt - ); - State.state.layerUpdater = updater; - - const source = new FeaturePipeline( - state.filteredLayers, - State.state.changes, - updater, - state.osmApiFeatureSource, - state.layoutToUse, - state.locationControl, - state.selectedElement + State.state.featurePipeline = new FeaturePipeline( + source => { + new ShowDataLayer( + { + features: source, + leafletMap: State.state.leafletMap, + layerToShow: source.layer.layerDef + } + ); + }, state ); - State.state.featurePipeline = source; - new ShowDataLayer( - source.features, - State.state.leafletMap, - State.state.layoutToUse - ); - - const selectedFeatureHandler = new SelectedFeatureHandler( - Hash.hash, - State.state.selectedElement, - source, - State.state.osmApiFeatureSource - ); - selectedFeatureHandler.zoomToSelectedFeature( - State.state.locationControl - ); - return source; + /* const selectedFeatureHandler = new SelectedFeatureHandler( + Hash.hash, + State.state.selectedElement, + source, + State.state.osmApiFeatureSource + ); + selectedFeatureHandler.zoomToSelectedFeature( + State.state.locationControl + );*/ } private static setupAllLayerElements() { // ------------- Setup the layers ------------------------------- - const source = InitUiElements.InitLayers(); + InitUiElements.InitLayers(); - new LeftControls(source).AttachTo("bottom-left"); + new LeftControls(State.state).AttachTo("bottom-left"); new RightControls().AttachTo("bottom-right"); // ------------------ Setup various other UI elements ------------ diff --git a/Logic/Actors/BackgroundLayerResetter.ts b/Logic/Actors/BackgroundLayerResetter.ts index 1a2175dfc..96c43bda3 100644 --- a/Logic/Actors/BackgroundLayerResetter.ts +++ b/Logic/Actors/BackgroundLayerResetter.ts @@ -6,7 +6,7 @@ import Loc from "../../Models/Loc"; /** * Sets the current background layer to a layer that is actually available */ -export default class LayerResetter { +export default class BackgroundLayerResetter { constructor(currentBackgroundLayer: UIEventSource, location: UIEventSource, diff --git a/Logic/Actors/OverpassFeatureSource.ts b/Logic/Actors/OverpassFeatureSource.ts index 4d4110935..e788c7616 100644 --- a/Logic/Actors/OverpassFeatureSource.ts +++ b/Logic/Actors/OverpassFeatureSource.ts @@ -3,14 +3,15 @@ import Loc from "../../Models/Loc"; import {Or} from "../Tags/Or"; import {Overpass} from "../Osm/Overpass"; import Bounds from "../../Models/Bounds"; -import FeatureSource from "../FeatureSource/FeatureSource"; +import FeatureSource, {FeatureSourceState} from "../FeatureSource/FeatureSource"; import {Utils} from "../../Utils"; import {TagsFilter} from "../Tags/TagsFilter"; import SimpleMetaTagger from "../SimpleMetaTagger"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import RelationsTracker from "../Osm/RelationsTracker"; -export default class OverpassFeatureSource implements FeatureSource { +export default class OverpassFeatureSource implements FeatureSource, FeatureSourceState { public readonly name = "OverpassFeatureSource" @@ -24,6 +25,9 @@ export default class OverpassFeatureSource implements FeatureSource { public readonly runningQuery: UIEventSource = new UIEventSource(false); public readonly timeout: UIEventSource = new UIEventSource(0); + public readonly relationsTracker: RelationsTracker; + + private readonly retries: UIEventSource = new UIEventSource(0); /** * The previous bounds for which the query has been run at the given zoom level @@ -33,56 +37,61 @@ export default class OverpassFeatureSource implements FeatureSource { * we start checking the bounds at the first zoom level the layer might operate. If in bounds - no reload needed, otherwise we continue walking down */ private readonly _previousBounds: Map = new Map(); - private readonly _location: UIEventSource; - private readonly _layoutToUse: UIEventSource; - private readonly _leafletMap: UIEventSource; - private readonly _interpreterUrl: UIEventSource; - private readonly _timeout: UIEventSource; + private readonly state: { + readonly locationControl: UIEventSource, + readonly layoutToUse: UIEventSource, + readonly leafletMap: any, + readonly overpassUrl: UIEventSource; + readonly overpassTimeout: UIEventSource; + } /** * The most important layer should go first, as that one gets first pick for the questions */ constructor( - location: UIEventSource, - layoutToUse: UIEventSource, - leafletMap: UIEventSource, - interpreterUrl: UIEventSource, - timeout: UIEventSource, - maxZoom = undefined) { - this._location = location; - this._layoutToUse = layoutToUse; - this._leafletMap = leafletMap; - this._interpreterUrl = interpreterUrl; - this._timeout = timeout; + state: { + readonly locationControl: UIEventSource, + readonly layoutToUse: UIEventSource, + readonly leafletMap: any, + readonly overpassUrl: UIEventSource; + readonly overpassTimeout: UIEventSource; + readonly overpassMaxZoom: UIEventSource + }) { + + + this.state = state + this.relationsTracker = new RelationsTracker() + const location = state.locationControl const self = this; this.sufficientlyZoomed = location.map(location => { if (location?.zoom === undefined) { return false; } - let minzoom = Math.min(...layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); - if(location.zoom < minzoom){ + let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); + if (location.zoom < minzoom) { return false; } - if(maxZoom !== undefined && location.zoom > maxZoom){ + const maxZoom = state.overpassMaxZoom.data + if (maxZoom !== undefined && location.zoom > maxZoom) { return false; } - + return true; - }, [layoutToUse] + }, [state.layoutToUse] ); for (let i = 0; i < 25; i++) { // This update removes all data on all layers -> erase the map on lower levels too this._previousBounds.set(i, []); } - layoutToUse.addCallback(() => { + state.layoutToUse.addCallback(() => { self.update() }); location.addCallback(() => { self.update() }); - leafletMap.addCallbackAndRunD(_ => { + state.leafletMap.addCallbackAndRunD(_ => { self.update(); }) } @@ -97,11 +106,11 @@ export default class OverpassFeatureSource implements FeatureSource { private GetFilter(): Overpass { let filters: TagsFilter[] = []; let extraScripts: string[] = []; - for (const layer of this._layoutToUse.data.layers) { + for (const layer of this.state.layoutToUse.data.layers) { if (typeof (layer) === "string") { throw "A layer was not expanded!" } - if (this._location.data.zoom < layer.minzoom) { + if (this.state.locationControl.data.zoom < layer.minzoom) { continue; } if (layer.doNotDownload) { @@ -141,7 +150,7 @@ export default class OverpassFeatureSource implements FeatureSource { if (filters.length + extraScripts.length === 0) { return undefined; } - return new Overpass(new Or(filters), extraScripts, this._interpreterUrl, this._timeout); + return new Overpass(new Or(filters), extraScripts, this.state.overpassUrl, this.state.overpassTimeout, this.relationsTracker); } private update(): void { @@ -155,21 +164,22 @@ export default class OverpassFeatureSource implements FeatureSource { return; } - const bounds = this._leafletMap.data?.getBounds()?.pad( this._layoutToUse.data.widenFactor); + const bounds = this.state.leafletMap.data?.getBounds()?.pad(this.state.layoutToUse.data.widenFactor); if (bounds === undefined) { return; } - const n = Math.min(90, bounds.getNorth() ); - const e = Math.min(180, bounds.getEast() ); + const n = Math.min(90, bounds.getNorth()); + const e = Math.min(180, bounds.getEast()); const s = Math.max(-90, bounds.getSouth()); const w = Math.max(-180, bounds.getWest()); const queryBounds = {north: n, east: e, south: s, west: w}; - const z = Math.floor(this._location.data.zoom ?? 0); + const z = Math.floor(this.state.locationControl.data.zoom ?? 0); const self = this; const overpass = this.GetFilter(); + if (overpass === undefined) { return; } @@ -181,14 +191,18 @@ export default class OverpassFeatureSource implements FeatureSource { const features = data.features.map(f => ({feature: f, freshness: date})); SimpleMetaTagger.objectMetaInfo.addMetaTags(features) - self.features.setData(features); + try{ + self.features.setData(features); + }catch(e){ + console.error("Got the overpass response, but could not process it: ", e, e.stack) + } self.runningQuery.setData(false); }, function (reason) { self.retries.data++; self.ForceRefresh(); self.timeout.setData(self.retries.data * 5); - console.log(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to ${reason}`); + console.error(`QUERY FAILED (retrying in ${5 * self.retries.data} sec) due to ${reason}`); self.retries.ping(); self.runningQuery.setData(false); @@ -222,7 +236,7 @@ export default class OverpassFeatureSource implements FeatureSource { return false; } - const b = this._leafletMap.data.getBounds(); + const b = this.state.leafletMap.data.getBounds(); return b.getSouth() >= bounds.south && b.getNorth() <= bounds.north && b.getEast() <= bounds.east && diff --git a/Logic/Actors/SelectedFeatureHandler.ts b/Logic/Actors/SelectedFeatureHandler.ts index 5a63d5cb2..07ff7f4a7 100644 --- a/Logic/Actors/SelectedFeatureHandler.ts +++ b/Logic/Actors/SelectedFeatureHandler.ts @@ -3,7 +3,7 @@ import FeatureSource from "../FeatureSource/FeatureSource"; import {OsmObject} from "../Osm/OsmObject"; import Loc from "../../Models/Loc"; import FeaturePipeline from "../FeatureSource/FeaturePipeline"; -import OsmApiFeatureSource from "../FeatureSource/OsmApiFeatureSource"; +import OsmApiFeatureSource from "../FeatureSource/Sources/OsmApiFeatureSource"; /** * Makes sure the hash shows the selected element and vice-versa. diff --git a/Logic/ContributorCount.ts b/Logic/ContributorCount.ts index 9c954bdfb..6a4c2da25 100644 --- a/Logic/ContributorCount.ts +++ b/Logic/ContributorCount.ts @@ -1,21 +1,49 @@ /// 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 { - public readonly Contributors: UIEventSource>; + public readonly Contributors: UIEventSource> = new UIEventSource>(new Map()); + private readonly state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource, locationControl: UIEventSource }; - constructor(featureSource: FeatureSource) { - this.Contributors = featureSource.features.map(features => { - const hist = new Map(); - for (const feature of features) { - const contributor = feature.feature.properties["_last_edit:contributor"] + constructor(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource, locationControl: UIEventSource }) { + this.state = state; + const self = this; + state.currentBounds.map(bbox => { + self.update(bbox) + }) + state.featurePipeline.runningQuery.addCallbackAndRun( + _ => self.update(state.currentBounds.data) + ) + + } + + private lastUpdate: Date = undefined; + + private update(bbox: BBox) { + if(bbox === undefined){ + return; + } + const now = new Date(); + if (this.lastUpdate !== undefined && ((now.getTime() - this.lastUpdate.getTime()) < 1000 * 60)) { + return; + } + console.log("Calculating contributors") + const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox) + const hist = new Map(); + for (const list of featuresList) { + for (const feature of list) { + const contributor = feature.properties["_last_edit:contributor"] const count = hist.get(contributor) ?? 0; hist.set(contributor, count + 1) } - return hist; - }) + } + this.Contributors.setData(hist) } } \ No newline at end of file diff --git a/Logic/ExtraFunction.ts b/Logic/ExtraFunction.ts index 4deb3e01f..dd75ac356 100644 --- a/Logic/ExtraFunction.ts +++ b/Logic/ExtraFunction.ts @@ -1,14 +1,24 @@ -import {GeoOperations} from "./GeoOperations"; +import {BBox, GeoOperations} from "./GeoOperations"; import Combine from "../UI/Base/Combine"; -import {Relation} from "./Osm/ExtractRelations"; +import RelationsTracker from "./Osm/RelationsTracker"; import State from "../State"; -import {Utils} from "../Utils"; import BaseUIElement from "../UI/BaseUIElement"; import List from "../UI/Base/List"; import Title from "../UI/Base/Title"; import {UIEventSourceTools} from "./UIEventSource"; import AspectedRouting from "./Osm/aspectedRouting"; +export interface ExtraFuncParams { + /** + * Gets all the features from the given layer within the given BBOX. + * Note that more features then requested can be given back. + * Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...] + */ + getFeaturesWithin: (layerId: string, bbox: BBox) => any[][], + memberships: RelationsTracker +} + + export class ExtraFunction { @@ -55,15 +65,20 @@ export class ExtraFunction { (params, feat) => { return (...layerIds: string[]) => { const result = [] + + const bbox = BBox.get(feat) + for (const layerId of layerIds) { - const otherLayer = params.featuresPerLayer.get(layerId); - if (otherLayer === undefined) { + const otherLayers = params.getFeaturesWithin(layerId, bbox) + if (otherLayers === undefined) { continue; } - if (otherLayer.length === 0) { + if (otherLayers.length === 0) { continue; } - result.push(...GeoOperations.calculateOverlap(feat, otherLayer)); + for (const otherLayer of otherLayers) { + result.push(...GeoOperations.calculateOverlap(feat, otherLayer)); + } } return result; } @@ -77,6 +92,9 @@ export class ExtraFunction { }, (featuresPerLayer, feature) => { return (arg0, lat) => { + if (arg0 === undefined) { + return undefined; + } if (typeof arg0 === "number") { // Feature._lon and ._lat is conveniently place by one of the other metatags return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]); @@ -103,7 +121,7 @@ export class ExtraFunction { args: ["list of features"] }, (params, feature) => { - return (features) => ExtraFunction.GetClosestNFeatures(params, feature, features)[0].feat + return (features) => ExtraFunction.GetClosestNFeatures(params, feature, features)?.[0]?.feat } ) @@ -113,12 +131,13 @@ export class ExtraFunction { doc: "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. " + "Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet laoded)\n\n" + "If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)", - args: ["list of features", "amount of features", "unique tag key (optional)"] + args: ["list of features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"] }, (params, feature) => { - return (features, amount, uniqueTag) => ExtraFunction.GetClosestNFeatures(params, feature, features, { + return (features, amount, uniqueTag, maxDistanceInMeters) => ExtraFunction.GetClosestNFeatures(params, feature, features, { maxFeatures: Number(amount), - uniqueTag: uniqueTag + uniqueTag: uniqueTag, + maxDistance: Number(maxDistanceInMeters) }) } ) @@ -131,8 +150,10 @@ export class ExtraFunction { "For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`", args: [] }, - (params, _) => { - return () => params.relations ?? []; + (params, feat) => { + return () => + params.memberships.knownRelations.data.get(feat.properties.id) ?? [] + } ) private static readonly AspectedRouting = new ExtraFunction( @@ -165,19 +186,19 @@ export class ExtraFunction { private readonly _name: string; private readonly _args: string[]; private readonly _doc: string; - private readonly _f: (params: { featuresPerLayer: Map, relations: { role: string, relation: Relation }[] }, feat: any) => any; + private readonly _f: (params: ExtraFuncParams, feat: any) => any; constructor(options: { name: string, doc: string, args: string[] }, - f: ((params: { featuresPerLayer: Map, relations: { role: string, relation: Relation }[] }, feat: any) => any)) { + f: ((params: ExtraFuncParams, feat: any) => any)) { this._name = options.name; this._doc = options.doc; this._args = options.args; this._f = f; } - public static FullPatchFeature(featuresPerLayer: Map, relations: { role: string, relation: Relation }[], feature) { + public static FullPatchFeature(params: ExtraFuncParams, feature) { for (const func of ExtraFunction.allFuncs) { - func.PatchFeature(featuresPerLayer, relations, feature); + func.PatchFeature(params, feature); } } @@ -198,121 +219,132 @@ export class ExtraFunction { } /** - * Gets the closes N features, sorted by ascending distance + * Gets the closes N features, sorted by ascending distance. + * + * @param params: The link to mapcomplete state + * @param feature: The central feature under consideration + * @param features: The other features + * @param options: maxFeatures: The maximum amount of features to be returned. Default: 1; uniqueTag: returned features are not allowed to have the same value for this key; maxDistance: stop searching if it is too far away (in meter). Default: 500m + * @constructor + * @private */ - private static GetClosestNFeatures(params, feature, features, options?: { maxFeatures?: number, uniqueTag?: string | undefined }): { feat: any, distance: number }[] { + private static GetClosestNFeatures(params: ExtraFuncParams, + feature: any, + features: string | any[], + options?: { maxFeatures?: number, uniqueTag?: string | undefined, maxDistance?: number }): { feat: any, distance: number }[] { const maxFeatures = options?.maxFeatures ?? 1 - const uniqueTag : string | undefined = options?.uniqueTag + const maxDistance = options?.maxDistance ?? 500 + const uniqueTag: string | undefined = options?.uniqueTag if (typeof features === "string") { const name = features - features = params.featuresPerLayer.get(features) - if (features === undefined) { - var keys = Utils.NoNull(Array.from(params.featuresPerLayer.keys())); - if (keys.length > 0) { - throw `No features defined for ${name}. Defined layers are ${keys.join(", ")}`; - } else { - // This is the first pass over an external dataset - // Other data probably still has to load! - return undefined; - } - - } + const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance)) + features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates)) + }else{ + features = [features] + } + if (features === undefined) { + return; } let closestFeatures: { feat: any, distance: number }[] = []; - for (const otherFeature of features) { - if (otherFeature == feature || otherFeature.id == feature.id) { - continue; // We ignore self - } - let distance = undefined; - if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) { - distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]); - } else { - distance = GeoOperations.distanceBetween( - GeoOperations.centerpointCoordinates(otherFeature), - [feature._lon, feature._lat] - ) - } - if (distance === undefined) { - throw "Undefined distance!" - } + for(const featureList of features) { + for (const otherFeature of featureList) { + if (otherFeature == feature || otherFeature.id == feature.id) { + continue; // We ignore self + } + let distance = undefined; + if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) { + distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]); + } else { + distance = GeoOperations.distanceBetween( + GeoOperations.centerpointCoordinates(otherFeature), + [feature._lon, feature._lat] + ) + } + if (distance === undefined) { + throw "Undefined distance!" + } + if (distance > maxDistance) { + continue + } - if (closestFeatures.length === 0) { - closestFeatures.push({ - feat: otherFeature, - distance: distance - }) - continue; - } + if (closestFeatures.length === 0) { + closestFeatures.push({ + feat: otherFeature, + distance: distance + }) + continue; + } - if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { - // The last feature of the list (and thus the furthest away is still closer - // No use for checking, as we already have plenty of features! - continue - } + if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { + // The last feature of the list (and thus the furthest away is still closer + // No use for checking, as we already have plenty of features! + continue + } - let targetIndex = closestFeatures.length - for (let i = 0; i < closestFeatures.length; i++) { - const closestFeature = closestFeatures[i]; + let targetIndex = closestFeatures.length + for (let i = 0; i < closestFeatures.length; i++) { + const closestFeature = closestFeatures[i]; - if (uniqueTag !== undefined) { - const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && - closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] - if (uniqueTagsMatch) { - targetIndex = -1 - if (closestFeature.distance > distance) { - // This is a very special situation: - // We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads') - // AT this point, we have found a closer segment with the same, identical tag - // so we replace directly - closestFeatures[i] = {feat: otherFeature, distance: distance} + if (uniqueTag !== undefined) { + const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && + closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] + if (uniqueTagsMatch) { + targetIndex = -1 + if (closestFeature.distance > distance) { + // This is a very special situation: + // We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads') + // AT this point, we have found a closer segment with the same, identical tag + // so we replace directly + closestFeatures[i] = {feat: otherFeature, distance: distance} + } + break; + } + } + + if (closestFeature.distance > distance) { + targetIndex = i + + if (uniqueTag !== undefined) { + const uniqueValue = otherFeature.properties[uniqueTag] + // We might still have some other values later one with the same uniquetag that have to be cleaned + for (let j = i; j < closestFeatures.length; j++) { + if (closestFeatures[j].feat.properties[uniqueTag] === uniqueValue) { + closestFeatures.splice(j, 1) + } + } } break; } } - if (closestFeature.distance > distance) { - targetIndex = i + if (targetIndex == -1) { + continue; // value is already swapped by the unique tag + } - if (uniqueTag !== undefined) { - const uniqueValue = otherFeature.properties[uniqueTag] - // We might still have some other values later one with the same uniquetag that have to be cleaned - for (let j = i; j < closestFeatures.length; j++) { - if(closestFeatures[j].feat.properties[uniqueTag] === uniqueValue){ - closestFeatures.splice(j, 1) - } - } + if (targetIndex < maxFeatures) { + // insert and drop one + closestFeatures.splice(targetIndex, 0, { + feat: otherFeature, + distance: distance + }) + if (closestFeatures.length >= maxFeatures) { + closestFeatures.splice(maxFeatures, 1) + } + } else { + // Overwrite the last element + closestFeatures[targetIndex] = { + feat: otherFeature, + distance: distance } - break; - } - } - if (targetIndex == -1) { - continue; // value is already swapped by the unique tag - } - - if (targetIndex < maxFeatures) { - // insert and drop one - closestFeatures.splice(targetIndex, 0, { - feat: otherFeature, - distance: distance - }) - if (closestFeatures.length >= maxFeatures) { - closestFeatures.splice(maxFeatures, 1) } - } else { - // Overwrite the last element - closestFeatures[targetIndex] = { - feat: otherFeature, - distance: distance - } - } } return closestFeatures; } - public PatchFeature(featuresPerLayer: Map, relations: { role: string, relation: Relation }[], feature: any) { - feature[this._name] = this._f({featuresPerLayer: featuresPerLayer, relations: relations}, feature) + public PatchFeature(params: ExtraFuncParams, feature: any) { + feature[this._name] = this._f(params, feature) } } diff --git a/Logic/FeatureSource/Actors/LocalStorageSaverActor.ts b/Logic/FeatureSource/Actors/LocalStorageSaverActor.ts index 9d0cb6e3b..9cb9ce7f2 100644 --- a/Logic/FeatureSource/Actors/LocalStorageSaverActor.ts +++ b/Logic/FeatureSource/Actors/LocalStorageSaverActor.ts @@ -1,11 +1,11 @@ -import {FeatureSourceForLayer} from "./FeatureSource"; -import {Utils} from "../../Utils"; - /*** * Saves all the features that are passed in to localstorage, so they can be retrieved on the next run * * Technically, more an Actor then a featuresource, but it fits more neatly this ay */ +import {FeatureSourceForLayer} from "../FeatureSource"; +import {Utils} from "../../../Utils"; + export default class LocalStorageSaverActor { public static readonly storageKey: string = "cached-features"; @@ -21,7 +21,6 @@ export default class LocalStorageSaverActor { try { localStorage.setItem(key, JSON.stringify(features)); - console.log("Saved ", features.length, "elements to", key) localStorage.setItem(key + "-time", JSON.stringify(now)) } catch (e) { console.warn("Could not save the features to local storage:", e) diff --git a/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts b/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts index b3b921195..240846470 100644 --- a/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts +++ b/Logic/FeatureSource/Actors/RegisteringAllFromFeatureSourceActor.ts @@ -1,6 +1,6 @@ -import FeatureSource from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import State from "../../State"; +import FeatureSource from "../FeatureSource"; +import {UIEventSource} from "../../UIEventSource"; +import State from "../../../State"; export default class RegisteringAllFromFeatureSourceActor { public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; diff --git a/Logic/FeatureSource/ChangeApplicator.ts b/Logic/FeatureSource/ChangeApplicator.ts index 9b4f2271d..0507c733d 100644 --- a/Logic/FeatureSource/ChangeApplicator.ts +++ b/Logic/FeatureSource/ChangeApplicator.ts @@ -1,10 +1,35 @@ -import FeatureSource from "./FeatureSource"; +import FeatureSource, {IndexedFeatureSource} from "./FeatureSource"; import {UIEventSource} from "../UIEventSource"; import {Changes} from "../Osm/Changes"; import {ChangeDescription} from "../Osm/Actions/ChangeDescription"; import {Utils} from "../../Utils"; import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject"; +/** + * A feature source containing exclusively new elements + */ +export class NewGeometryChangeApplicatorFeatureSource implements FeatureSource{ + + public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; + public readonly name: string = "newFeatures"; + constructor(changes: Changes) { + const seenChanges = new Set(); + changes.pendingChanges.addCallbackAndRunD(changes => { + for (const change of changes) { + if(seenChanges.has(change)){ + continue + } + seenChanges.add(change) + + if(change.id < 0){ + // This is a new object! + } + + } + }) + } + +} /** * Applies changes from 'Changes' onto a featureSource @@ -12,10 +37,18 @@ import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject"; export default class ChangeApplicator implements FeatureSource { public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly name: string; + private readonly source: IndexedFeatureSource; + private readonly changes: Changes; + private readonly mode?: { + generateNewGeometries: boolean + }; - constructor(source: FeatureSource, changes: Changes, mode?: { + constructor(source: IndexedFeatureSource, changes: Changes, mode?: { generateNewGeometries: boolean }) { + this.source = source; + this.changes = changes; + this.mode = mode; this.name = "ChangesApplied(" + source.name + ")" this.features = source.features @@ -26,7 +59,7 @@ export default class ChangeApplicator implements FeatureSource { if (runningUpdate) { return; // No need to ping again } - ChangeApplicator.ApplyChanges(features, changes.pendingChanges.data, mode) + self.ApplyChanges() seenChanges.clear() }) @@ -34,19 +67,20 @@ export default class ChangeApplicator implements FeatureSource { runningUpdate = true; changes = changes.filter(ch => !seenChanges.has(ch)) changes.forEach(c => seenChanges.add(c)) - ChangeApplicator.ApplyChanges(self.features.data, changes, mode) + self.ApplyChanges() source.features.ping() runningUpdate = false; }) - - } /** * Returns true if the geometry is changed and the source should be pinged */ - private static ApplyChanges(features: { feature: any; freshness: Date }[], cs: ChangeDescription[], mode: { generateNewGeometries: boolean }): boolean { + private ApplyChanges(): boolean { + const cs = this.changes.pendingChanges.data + const features = this.source.features.data + const loadedIds = this.source.containedIds if (cs.length === 0 || features === undefined) { return; } @@ -56,12 +90,18 @@ export default class ChangeApplicator implements FeatureSource { const changesPerId: Map = new Map() for (const c of cs) { const id = c.type + "/" + c.id + if (!loadedIds.has(id)) { + continue + } if (!changesPerId.has(id)) { changesPerId.set(id, []) } changesPerId.get(id).push(c) } - + if (changesPerId.size === 0) { + // The current feature source set doesn't contain any changed feature, so we can safely skip + return; + } const now = new Date() @@ -77,7 +117,7 @@ export default class ChangeApplicator implements FeatureSource { // First, create the new features - they have a negative ID // We don't set the properties yet though - if (mode?.generateNewGeometries) { + if (this.mode?.generateNewGeometries) { changesPerId.forEach(cs => { cs .forEach(change => { diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 5e91b4567..ec4a052be 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -1,95 +1,191 @@ -import FilteringFeatureSource from "../FeatureSource/FilteringFeatureSource"; -import FeatureSourceMerger from "../FeatureSource/FeatureSourceMerger"; -import RememberingSource from "../FeatureSource/RememberingSource"; -import WayHandlingApplyingFeatureSource from "../FeatureSource/WayHandlingApplyingFeatureSource"; -import FeatureDuplicatorPerLayer from "../FeatureSource/FeatureDuplicatorPerLayer"; -import FeatureSource from "../FeatureSource/FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import LocalStorageSaver from "./LocalStorageSaver"; -import LocalStorageSource from "./LocalStorageSource"; -import Loc from "../../Models/Loc"; -import GeoJsonSource from "./GeoJsonSource"; -import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource"; -import RegisteringFeatureSource from "./RegisteringFeatureSource"; -import FilteredLayer from "../../Models/FilteredLayer"; -import {Changes} from "../Osm/Changes"; -import ChangeApplicator from "./ChangeApplicator"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import FilteringFeatureSource from "./Sources/FilteringFeatureSource"; +import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter"; +import FeatureSource, {FeatureSourceForLayer, FeatureSourceState, Tiled} from "./FeatureSource"; +import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource"; +import {UIEventSource} from "../UIEventSource"; +import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy"; +import FilteredLayer from "../../Models/FilteredLayer"; +import MetaTagging from "../MetaTagging"; +import RememberingSource from "./Sources/RememberingSource"; +import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; +import {Changes} from "../Osm/Changes"; +import GeoJsonSource from "./Sources/GeoJsonSource"; +import Loc from "../../Models/Loc"; +import WayHandlingApplyingFeatureSource from "./Sources/WayHandlingApplyingFeatureSource"; +import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor"; +import {Utils} from "../../Utils"; +import TiledFromLocalStorageSource from "./TiledFeatureSource/TiledFromLocalStorageSource"; +import LocalStorageSaverActor from "./Actors/LocalStorageSaverActor"; +import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource"; +import {BBox} from "../GeoOperations"; +import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger"; +import RelationsTracker from "../Osm/RelationsTracker"; -export default class FeaturePipeline implements FeatureSource { - public features: UIEventSource<{ feature: any; freshness: Date }[]>; +export default class FeaturePipeline implements FeatureSourceState { - public readonly name = "FeaturePipeline" + public readonly sufficientlyZoomed: UIEventSource; + public readonly runningQuery: UIEventSource; + public readonly timeout: UIEventSource; + public readonly somethingLoaded: UIEventSource = new UIEventSource(false) - constructor(flayers: UIEventSource, - changes: Changes, - updater: FeatureSource, - fromOsmApi: FeatureSource, - layout: UIEventSource, - locationControl: UIEventSource, - selectedElement: UIEventSource) { + private readonly overpassUpdater: OverpassFeatureSource + private readonly relationTracker: RelationsTracker + private readonly perLayerHierarchy: Map; + constructor( + handleFeatureSource: (source: FeatureSourceForLayer) => void, + state: { + osmApiFeatureSource: FeatureSource, + filteredLayers: UIEventSource, + locationControl: UIEventSource, + selectedElement: UIEventSource, + changes: Changes, + layoutToUse: UIEventSource, + leafletMap: any, + readonly overpassUrl: UIEventSource; + readonly overpassTimeout: UIEventSource; + readonly overpassMaxZoom: UIEventSource; + }) { - const allLoadedFeatures = new UIEventSource<{ feature: any; freshness: Date }[]>([]) + const self = this + const updater = new OverpassFeatureSource(state); + this.overpassUpdater = updater; + this.sufficientlyZoomed = updater.sufficientlyZoomed + this.runningQuery = updater.runningQuery + this.timeout = updater.timeout + this.relationTracker = updater.relationsTracker + // Register everything in the state' 'AllElements' + new RegisteringAllFromFeatureSourceActor(updater) - // 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 perLayerHierarchy = new Map() + this.perLayerHierarchy = perLayerHierarchy - // AT last, the metaTagging also needs to be run _after_ the duplicatorPerLayer - const amendedOverpassSource = - new RememberingSource( - new LocalStorageSaver( - new MetaTaggingFeatureSource(allLoadedFeatures, - new FeatureDuplicatorPerLayer(flayers, - new RegisteringFeatureSource( - new ChangeApplicator( - updater, changes - )) - )), layout)); + const patchedHandleFeatureSource = function (src: FeatureSourceForLayer) { + // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile + const srcFiltered = + new FilteringFeatureSource(state, + new WayHandlingApplyingFeatureSource( + src, + ) + ) + handleFeatureSource(srcFiltered) + self.somethingLoaded.setData(true) + }; - const geojsonSources: FeatureSource [] = GeoJsonSource - .ConstructMultiSource(flayers.data, locationControl) - .map(geojsonSource => { - let source = new RegisteringFeatureSource( - new FeatureDuplicatorPerLayer(flayers, - new ChangeApplicator(geojsonSource, changes))); - if (!geojsonSource.isOsmCache) { - source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); - } - return source - }); + function addToHierarchy(src: FeatureSource & Tiled, layerId: string) { + perLayerHierarchy.get(layerId).registerTile(src) + } - const amendedLocalStorageSource = - new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new ChangeApplicator(new LocalStorageSource(layout), changes)) - )); + for (const filteredLayer of state.filteredLayers.data) { + const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => patchedHandleFeatureSource(tile)) + const id = filteredLayer.layerDef.id + perLayerHierarchy.set(id, hierarchy) + const source = filteredLayer.layerDef.source - const amendedOsmApiSource = new RememberingSource( - new MetaTaggingFeatureSource(allLoadedFeatures, - new FeatureDuplicatorPerLayer(flayers, - new RegisteringFeatureSource(new ChangeApplicator(fromOsmApi, changes, - { - // We lump in the new points here - generateNewGeometries: true + if (source.geojsonSource === undefined) { + // This is an OSM layer + // We load the cached values and register them + // Getting data from upstream happens a bit lower + new TiledFromLocalStorageSource(filteredLayer, + (src) => { + new RegisteringAllFromFeatureSourceActor(src) + hierarchy.registerTile(src); + }, state) + continue + } + + if (source.geojsonZoomLevel === undefined) { + // This is a 'load everything at once' geojson layer + // We split them up into tiles + const src = new GeoJsonSource(filteredLayer) + TiledFeatureSource.createHierarchy(src, { + layer: src.layer, + registerTile: (tile) => { + new RegisteringAllFromFeatureSourceActor(tile) + addToHierarchy(tile, id) + } + }) + } else { + new DynamicGeoJsonTileSource( + filteredLayer, + src => TiledFeatureSource.createHierarchy(src, { + layer: src.layer, + registerTile: (tile) => { + new RegisteringAllFromFeatureSourceActor(tile) + addToHierarchy(tile, id) } - ))))); + }), + state + ) + } - const merged = - new FeatureSourceMerger([ - amendedOverpassSource, - amendedOsmApiSource, - amendedLocalStorageSource, - ...geojsonSources - ]); + } - merged.features.syncWith(allLoadedFeatures) + // Actually load data from the overpass source + + new PerLayerFeatureSourceSplitter(state.filteredLayers, + (source) => TiledFeatureSource.createHierarchy(source, { + layer: source.layer, + registerTile: (tile) => { + // We save the tile data for the given layer to local storage + const [z, x, y] = Utils.tile_from_index(tile.tileIndex) + new LocalStorageSaverActor(tile, x, y, z) + addToHierarchy(tile, source.layer.layerDef.id); + } + }), new RememberingSource(updater)) + + + // Whenever fresh data comes in, we need to update the metatagging + updater.features.addCallback(_ => { + self.updateAllMetaTagging() + }) - this.features = new WayHandlingApplyingFeatureSource(flayers, - new FilteringFeatureSource( - flayers, - locationControl, - selectedElement, - merged - )).features; } + private updateAllMetaTagging() { + console.log("Updating the meta tagging") + const self = this; + this.perLayerHierarchy.forEach(hierarchy => { + hierarchy.loadedTiles.forEach(src => { + MetaTagging.addMetatags( + src.features.data, + { + memberships: this.relationTracker, + getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox) + }, + src.layer.layerDef + ) + }) + }) + + } + + public GetAllFeaturesWithin(bbox: BBox): any[][]{ + const self = this + const tiles = [] + Array.from(this.perLayerHierarchy.keys()) + .forEach(key => tiles.push(...self.GetFeaturesWithin(key, bbox))) + return tiles; + } + + public GetFeaturesWithin(layerId: string, bbox: BBox): any[][]{ + const requestedHierarchy = this.perLayerHierarchy.get(layerId) + if (requestedHierarchy === undefined) { + return undefined; + } + return TileHierarchyTools.getTiles(requestedHierarchy, bbox) + .filter(featureSource => featureSource.features?.data !== undefined) + .map(featureSource => featureSource.features.data.map(fs => fs.feature)) + } + + public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void){ + Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => { + TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile) + }) + } + + public ForceRefresh() { + this.overpassUpdater.ForceRefresh() + } } \ No newline at end of file diff --git a/Logic/FeatureSource/FeatureSource.ts b/Logic/FeatureSource/FeatureSource.ts index 624658e79..791882e11 100644 --- a/Logic/FeatureSource/FeatureSource.ts +++ b/Logic/FeatureSource/FeatureSource.ts @@ -1,5 +1,7 @@ import {UIEventSource} from "../UIEventSource"; import {Utils} from "../../Utils"; +import FilteredLayer from "../../Models/FilteredLayer"; +import {BBox} from "../GeoOperations"; export default interface FeatureSource { features: UIEventSource<{ feature: any, freshness: Date }[]>; @@ -9,38 +11,30 @@ export default interface FeatureSource { name: string; } -export class FeatureSourceUtils { +export interface Tiled { + tileIndex: number, + bbox: BBox +} - /** - * Exports given featurePipeline as a geojson FeatureLists (downloads as a json) - * @param featurePipeline The FeaturePipeline you want to export - * @param options The options object - * @param options.metadata True if you want to include the MapComplete metadata, false otherwise - */ - public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) { - let defaults = { - metadata: false, - } - options = Utils.setDefaults(options, defaults); +/** + * A feature source which only contains features for the defined layer + */ +export interface FeatureSourceForLayer extends FeatureSource{ + readonly layer: FilteredLayer +} - // Select all features, ignore the freshness and other data - let featureList: any[] = featurePipeline.features.data.map((feature) => - JSON.parse(JSON.stringify((feature.feature)))); // Make a deep copy! +/** + * A feature source which is aware of the indexes it contains + */ +export interface IndexedFeatureSource extends FeatureSource { + readonly containedIds: UIEventSource> +} - if (!options.metadata) { - for (let i = 0; i < featureList.length; i++) { - let feature = featureList[i]; - for (let property in feature.properties) { - if (property[0] == "_" && property !== "_lat" && property !== "_lon") { - delete featureList[i]["properties"][property]; - } - } - } - } - return {type: "FeatureCollection", features: featureList} - - - } - - -} \ No newline at end of file +/** + * A feature source which has some extra data about it's state + */ +export interface FeatureSourceState { + readonly sufficientlyZoomed: UIEventSource; + readonly runningQuery: UIEventSource; + readonly timeout: UIEventSource; +} diff --git a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts index 296f318b7..bb39660fa 100644 --- a/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts +++ b/Logic/FeatureSource/PerLayerFeatureSourceSplitter.ts @@ -1,8 +1,7 @@ -import FeatureSource from "./FeatureSource"; +import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource"; import {UIEventSource} from "../UIEventSource"; import FilteredLayer from "../../Models/FilteredLayer"; -import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; -import SimpleFeatureSource from "./SimpleFeatureSource"; +import SimpleFeatureSource from "./Sources/SimpleFeatureSource"; /** @@ -13,17 +12,17 @@ import SimpleFeatureSource from "./SimpleFeatureSource"; export default class PerLayerFeatureSourceSplitter { constructor(layers: UIEventSource, - handleLayerData: (source: FeatureSource) => void, - upstream: OverpassFeatureSource) { + handleLayerData: (source: FeatureSourceForLayer) => void, + upstream: FeatureSource) { - const knownLayers = new Map() + const knownLayers = new Map() function update() { const features = upstream.features.data; if (features === undefined) { return; } - if(layers.data === undefined){ + if (layers.data === undefined) { return; } @@ -69,19 +68,16 @@ export default class PerLayerFeatureSourceSplitter { if (featureSource === undefined) { // Not yet initialized - now is a good time featureSource = new SimpleFeatureSource(layer) + featureSource.features.setData(features) knownLayers.set(id, featureSource) handleLayerData(featureSource) + } else { + featureSource.features.setData(features) } - featureSource.features.setData(features) } - - - upstream.features.addCallbackAndRunD(_ => update()) - layers.addCallbackAndRunD(_ => update()) - } - - layers.addCallbackAndRunD(_ => update()) + + layers.addCallback(_ => update()) upstream.features.addCallbackAndRunD(_ => update()) } } \ No newline at end of file diff --git a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts index 17d6db690..574258455 100644 --- a/Logic/FeatureSource/Sources/FeatureSourceMerger.ts +++ b/Logic/FeatureSource/Sources/FeatureSourceMerger.ts @@ -1,22 +1,28 @@ -import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import FilteredLayer from "../../Models/FilteredLayer"; - /** * Merges features from different featureSources for a single layer * Uses the freshest feature available in the case multiple sources offer data with the same identifier */ -export default class FeatureSourceMerger implements FeatureSourceForLayer { +import {UIEventSource} from "../../UIEventSource"; +import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource"; +import FilteredLayer from "../../../Models/FilteredLayer"; +import {BBox} from "../../GeoOperations"; +import {Utils} from "../../../Utils"; + +export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled { public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); public readonly name; public readonly layer: FilteredLayer private readonly _sources: UIEventSource; + public readonly tileIndex: number; + public readonly bbox: BBox; - constructor(layer: FilteredLayer ,sources: UIEventSource) { + constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource) { + this.tileIndex = tileIndex; + this.bbox = bbox; this._sources = sources; this.layer = layer; - this.name = "SourceMerger" + this.name = "FeatureSourceMerger("+layer.layerDef.id+", "+Utils.tile_from_index(tileIndex).join(",")+")" const self = this; const handledSources = new Set(); diff --git a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts index 67a12af35..6848f9f26 100644 --- a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts @@ -1,13 +1,13 @@ -import {FeatureSourceForLayer} from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import Hash from "../Web/Hash"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; -import FilteredLayer from "../../Models/FilteredLayer"; +import {UIEventSource} from "../../UIEventSource"; +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; +import FilteredLayer from "../../../Models/FilteredLayer"; +import {FeatureSourceForLayer} from "../FeatureSource"; +import Hash from "../../Web/Hash"; export default class FilteringFeatureSource implements FeatureSourceForLayer { public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); - public readonly name = "FilteringFeatureSource"; + public readonly name; public readonly layer: FilteredLayer; constructor( @@ -18,6 +18,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer { upstream: FeatureSourceForLayer ) { const self = this; + this.name = "FilteringFeatureSource("+upstream.name+")" this.layer = upstream.layer; const layer = upstream.layer; diff --git a/Logic/FeatureSource/Sources/GeoJsonSource.ts b/Logic/FeatureSource/Sources/GeoJsonSource.ts index 165f672cf..e6d24fbca 100644 --- a/Logic/FeatureSource/Sources/GeoJsonSource.ts +++ b/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -1,14 +1,14 @@ -import {FeatureSourceForLayer} from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import {Utils} from "../../Utils"; -import FilteredLayer from "../../Models/FilteredLayer"; -import {control} from "leaflet"; - - /** * Fetches a geojson file somewhere and passes it along */ -export default class GeoJsonSource implements FeatureSourceForLayer { +import {UIEventSource} from "../../UIEventSource"; +import FilteredLayer from "../../../Models/FilteredLayer"; +import {Utils} from "../../../Utils"; +import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; +import {BBox} from "../../GeoOperations"; + + +export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly name; @@ -17,6 +17,8 @@ export default class GeoJsonSource implements FeatureSourceForLayer { private readonly seenids: Set = new Set() public readonly layer: FilteredLayer; + public readonly tileIndex + public readonly bbox; public constructor(flayer: FilteredLayer, zxy?: [number, number, number]) { @@ -28,10 +30,16 @@ export default class GeoJsonSource implements FeatureSourceForLayer { this.layer = flayer; let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); if (zxy !== undefined) { + const [z, x, y] = zxy; url = url - .replace('{z}', "" + zxy[0]) - .replace('{x}', "" + zxy[1]) - .replace('{y}', "" + zxy[2]) + .replace('{z}', "" + z) + .replace('{x}', "" + x) + .replace('{y}', "" + y) + this.tileIndex = Utils.tile_index(z, x, y) + this.bbox = BBox.fromTile(z, x, y) + } else { + this.tileIndex = Utils.tile_index(0, 0, 0) + this.bbox = BBox.global; } this.name = "GeoJsonSource of " + url; diff --git a/Logic/FeatureSource/Sources/OsmApiFeatureSource.ts b/Logic/FeatureSource/Sources/OsmApiFeatureSource.ts index de3157da2..c1c846602 100644 --- a/Logic/FeatureSource/Sources/OsmApiFeatureSource.ts +++ b/Logic/FeatureSource/Sources/OsmApiFeatureSource.ts @@ -1,9 +1,9 @@ -import FeatureSource from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import {OsmObject} from "../Osm/OsmObject"; -import {Utils} from "../../Utils"; -import Loc from "../../Models/Loc"; -import FilteredLayer from "../../Models/FilteredLayer"; +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 { diff --git a/Logic/FeatureSource/Sources/RememberingSource.ts b/Logic/FeatureSource/Sources/RememberingSource.ts index 42b0b0ba3..99f422478 100644 --- a/Logic/FeatureSource/Sources/RememberingSource.ts +++ b/Logic/FeatureSource/Sources/RememberingSource.ts @@ -1,11 +1,10 @@ - -import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import FilteredLayer from "../../Models/FilteredLayer"; /** * Every previously added point is remembered, but new points are added. * Data coming from upstream will always overwrite a previous value */ +import FeatureSource from "../FeatureSource"; +import {UIEventSource} from "../../UIEventSource"; + export default class RememberingSource implements FeatureSource { public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; diff --git a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts index 6237a2ddb..d4c316ec4 100644 --- a/Logic/FeatureSource/Sources/SimpleFeatureSource.ts +++ b/Logic/FeatureSource/Sources/SimpleFeatureSource.ts @@ -1,6 +1,6 @@ -import {FeatureSourceForLayer} from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import FilteredLayer from "../../Models/FilteredLayer"; +import {UIEventSource} from "../../UIEventSource"; +import FilteredLayer from "../../../Models/FilteredLayer"; +import {FeatureSourceForLayer} from "../FeatureSource"; export default class SimpleFeatureSource implements FeatureSourceForLayer { public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); diff --git a/Logic/FeatureSource/Sources/StaticFeatureSource.ts b/Logic/FeatureSource/Sources/StaticFeatureSource.ts index 7ffe1b02a..0cc58d656 100644 --- a/Logic/FeatureSource/Sources/StaticFeatureSource.ts +++ b/Logic/FeatureSource/Sources/StaticFeatureSource.ts @@ -8,12 +8,19 @@ export default class StaticFeatureSource implements FeatureSource { public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly name: string = "StaticFeatureSource" - constructor(features: any[]) { + constructor(features: any[] | UIEventSource, useFeaturesDirectly = false) { const now = new Date(); - this.features = new UIEventSource(features.map(f => ({ - feature: f, - freshness: now - }))) + if(useFeaturesDirectly){ + // @ts-ignore + this.features = features + }else if (features instanceof UIEventSource) { + this.features = features.map(features => features.map(f => ({feature: f, freshness: now}))) + } else { + this.features = new UIEventSource(features.map(f => ({ + feature: f, + freshness: now + }))) + } } diff --git a/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts b/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts index 54e1137a0..37ee94b8a 100644 --- a/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts +++ b/Logic/FeatureSource/Sources/WayHandlingApplyingFeatureSource.ts @@ -1,18 +1,18 @@ -import {FeatureSourceForLayer} from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import {GeoOperations} from "../GeoOperations"; -import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; - /** * This is the part of the pipeline which introduces extra points at the center of an area (but only if this is demanded by the wayhandling) */ +import {UIEventSource} from "../../UIEventSource"; +import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; +import {GeoOperations} from "../../GeoOperations"; +import {FeatureSourceForLayer} from "../FeatureSource"; + export default class WayHandlingApplyingFeatureSource implements FeatureSourceForLayer { public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly name; public readonly layer; constructor(upstream: FeatureSourceForLayer) { - this.name = "Wayhandling of " + upstream.name; + this.name = "Wayhandling(" + upstream.name+")"; this.layer = upstream.layer const layer = upstream.layer.layerDef; diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts index e69de29bb..357db85d4 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts @@ -0,0 +1,63 @@ +import FilteredLayer from "../../../Models/FilteredLayer"; +import {FeatureSourceForLayer} from "../FeatureSource"; +import {UIEventSource} from "../../UIEventSource"; +import Loc from "../../../Models/Loc"; +import DynamicTileSource from "./DynamicTileSource"; +import {Utils} from "../../../Utils"; +import GeoJsonSource from "../Sources/GeoJsonSource"; + +export default class DynamicGeoJsonTileSource extends DynamicTileSource { + constructor(layer: FilteredLayer, + registerLayer: (layer: FeatureSourceForLayer) => void, + state: { + locationControl: UIEventSource + leafletMap: any + }) { + const source = layer.layerDef.source + if (source.geojsonZoomLevel === undefined) { + throw "Invalid layer: geojsonZoomLevel expected" + } + if (source.geojsonSource === undefined) { + throw "Invalid layer: geojsonSource expected" + } + + const whitelistUrl = source.geojsonSource.replace("{z}_{x}_{y}.geojson", "overview.json") + .replace("{layer}",layer.layerDef.id) + + let whitelist = undefined + Utils.downloadJson(whitelistUrl).then( + json => { + const data = new Map>(); + for (const x in json) { + data.set(Number(x), new Set(json[x])) + } + whitelist = data + } + ).catch(err => { + console.warn("No whitelist found for ", layer.layerDef.id, err) + }) + + super( + layer, + source.geojsonZoomLevel, + (zxy) => { + if(whitelist !== undefined){ + const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2]) + if(!isWhiteListed){ + return undefined; + } + } + + const src = new GeoJsonSource( + layer, + zxy + ) + registerLayer(src) + return src + }, + state + ); + + } + +} \ No newline at end of file diff --git a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts index 931b85d3c..43021587c 100644 --- a/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts @@ -1,22 +1,24 @@ -/*** - * A tiled source which dynamically loads the required tiles - */ + import State from "../../../State"; import FilteredLayer from "../../../Models/FilteredLayer"; -import {FeatureSourceForLayer} from "../FeatureSource"; +import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {Utils} from "../../../Utils"; import {UIEventSource} from "../../UIEventSource"; import Loc from "../../../Models/Loc"; +import TileHierarchy from "./TileHierarchy"; -export default class DynamicTileSource { +/*** + * A tiled source which dynamically loads the required tiles at a fixed zoom level + */ +export default class DynamicTileSource implements TileHierarchy { private readonly _loadedTiles = new Set(); - - public readonly existingTiles: Map> = new Map>() + + public readonly loadedTiles: Map; constructor( layer: FilteredLayer, zoomlevel: number, - constructTile: (xy: [number, number]) => FeatureSourceForLayer, + constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled), state: { locationControl: UIEventSource leafletMap: any @@ -24,6 +26,8 @@ export default class DynamicTileSource { ) { state = State.state const self = this; + + this.loadedTiles = new Map() const neededTiles = state.locationControl.map( location => { if (!layer.isDisplayed.data) { @@ -45,28 +49,30 @@ export default class DynamicTileSource { const tileRange = Utils.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) const needed = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) - if(needed.length === 0){ + if (needed.length === 0) { return undefined } return needed } , [layer.isDisplayed, state.leafletMap]).stabilized(250); - + neededTiles.addCallbackAndRunD(neededIndexes => { + console.log("Tiled geojson source ",layer.layerDef.id," needs", neededIndexes) + if (neededIndexes === undefined) { + return; + } for (const neededIndex of neededIndexes) { self._loadedTiles.add(neededIndex) - const xy = Utils.tile_from_index(zoomlevel, neededIndex) - const src = constructTile(xy) - let xmap = self.existingTiles.get(xy[0]) - if(xmap === undefined){ - xmap = new Map() - self.existingTiles.set(xy[0], xmap) + const src = constructTile( Utils.tile_from_index(neededIndex)) + if(src !== undefined){ + self.loadedTiles.set(neededIndex, src) } - xmap.set(xy[1], src) } }) } -} \ No newline at end of file + +} + diff --git a/Logic/FeatureSource/TiledFeatureSource/README.md b/Logic/FeatureSource/TiledFeatureSource/README.md index 36b0c1b01..93b51c6ad 100644 --- a/Logic/FeatureSource/TiledFeatureSource/README.md +++ b/Logic/FeatureSource/TiledFeatureSource/README.md @@ -1,3 +1,27 @@ Data in MapComplete can come from multiple sources. -In order to keep thins snappy, they are distributed over a tiled database \ No newline at end of file +Currently, they are: + +- The Overpass-API +- The OSM-API +- One or more GeoJSON files. This can be a single file or a set of tiled geojson files +- LocalStorage, containing features from a previous visit +- Changes made by the user introducing new features + +When the data enters from Overpass or from the OSM-API, they are first distributed per layer: + +OVERPASS | ---PerLayerFeatureSource---> FeatureSourceForLayer[] +OSM | + +The GeoJSon files (not tiled) are then added to this list + +A single FeatureSourcePerLayer is then further handled by splitting it into a tile hierarchy. + + + +In order to keep thins snappy, they are distributed over a tiled database per layer. + + +## Notes + +`cached-featuresbookcases` is the old key used `cahced-features{themeid}` and should be cleaned up \ No newline at end of file diff --git a/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts b/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts index e69de29bb..d905d2f65 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TileHierarchy.ts @@ -0,0 +1,25 @@ +import FeatureSource, {Tiled} from "../FeatureSource"; +import {BBox} from "../../GeoOperations"; + +export default interface TileHierarchy { + + /** + * A mapping from 'tile_index' to the actual tile featrues + */ + loadedTiles: Map + +} + +export class TileHierarchyTools { + + public static getTiles(hierarchy: TileHierarchy, bbox: BBox): T[] { + const result = [] + hierarchy.loadedTiles.forEach((tile) => { + if (tile.bbox.overlapsWith(bbox)) { + result.push(tile) + } + }) + return result; + } + +} \ No newline at end of file diff --git a/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts b/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts index 768a935fc..10d0c1742 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TileHierarchyMerger.ts @@ -1,10 +1,10 @@ -import TileHierarchy from "./TiledFeatureSource/TileHierarchy"; -import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource"; -import {UIEventSource} from "../UIEventSource"; -import FilteredLayer from "../../Models/FilteredLayer"; -import FeatureSourceMerger from "./Sources/FeatureSourceMerger"; -import {BBox} from "../GeoOperations"; -import {Utils} from "../../Utils"; +import TileHierarchy from "./TileHierarchy"; +import {UIEventSource} from "../../UIEventSource"; +import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource"; +import FilteredLayer from "../../../Models/FilteredLayer"; +import {Utils} from "../../../Utils"; +import {BBox} from "../../GeoOperations"; +import FeatureSourceMerger from "../Sources/FeatureSourceMerger"; export class TileHierarchyMerger implements TileHierarchy { public readonly loadedTiles: Map = new Map(); @@ -24,8 +24,9 @@ export class TileHierarchyMerger implements TileHierarchy { + public readonly z: number; + public readonly x: number; + public readonly y: number; + public readonly parent: TiledFeatureSource; + public readonly root: TiledFeatureSource + public readonly layer: FilteredLayer; + /* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile. + * Only defined on the root element! + */ + public readonly loadedTiles: Map = undefined; + + public readonly maxFeatureCount: number; + public readonly name; + public readonly features: UIEventSource<{ feature: any, freshness: Date }[]> + public readonly containedIds: UIEventSource> + + public readonly bbox: BBox; + private upper_left: TiledFeatureSource + private upper_right: TiledFeatureSource + private lower_left: TiledFeatureSource + private lower_right: TiledFeatureSource + private readonly maxzoom: number; + private readonly options: TiledFeatureSourceOptions + public readonly tileIndex: number; + + private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) { + this.z = z; + this.x = x; + this.y = y; + this.bbox = BBox.fromTile(z, x, y) + this.tileIndex = Utils.tile_index(z, x, y) + this.name = `TiledFeatureSource(${z},${x},${y})` + this.parent = parent; + this.layer = options.layer + options = options ?? {} + this.maxFeatureCount = options?.maxFeatureCount ?? 500; + this.maxzoom = options.maxZoomLevel ?? 18 + this.options = options; + if (parent === undefined) { + throw "Parent is not allowed to be undefined. Use null instead" + } + if (parent === null && z !== 0 && x !== 0 && y !== 0) { + throw "Invalid root tile: z, x and y should all be null" + } + if (parent === null) { + this.root = this; + this.loadedTiles = new Map() + } else { + this.root = this.parent.root; + this.loadedTiles = this.root.loadedTiles; + const i = Utils.tile_index(z, x, y) + this.root.loadedTiles.set(i, this) + } + this.features = new UIEventSource([]) + this.containedIds = this.features.map(features => { + if (features === undefined) { + return undefined; + } + return new Set(features.map(f => f.feature.properties.id)) + }) + + // We register this tile, but only when there is some data in it + if (this.options.registerTile !== undefined) { + this.features.addCallbackAndRunD(features => { + if (features.length === 0) { + return; + } + this.options.registerTile(this) + return true; + }) + } + + + } + + public static createHierarchy(features: FeatureSource, options?: TiledFeatureSourceOptions): TiledFeatureSource { + const root = new TiledFeatureSource(0, 0, 0, null, options) + features.features?.addCallbackAndRunD(feats => root.addFeatures(feats)) + return root; + } + + private isSplitNeeded(featureCount: number){ + if(this.upper_left !== undefined){ + // This tile has been split previously, so we keep on splitting + return true; + } + if(this.z >= this.maxzoom){ + // We are not allowed to split any further + return false + } + if(this.options.minZoomLevel !== undefined && this.z < this.options.minZoomLevel){ + // We must have at least this zoom level before we are allowed to start splitting + return true + } + + // To much features - we split + return featureCount > this.maxFeatureCount + + + } + + /*** + * Adds the list of features to this hierarchy. + * If there are too much features, the list will be broken down and distributed over the subtiles (only retaining features that don't fit a subtile on this level) + * @param features + * @private + */ + private addFeatures(features: { feature: any, freshness: Date }[]) { + if (features === undefined || features.length === 0) { + return; + } + + if (!this.isSplitNeeded(features.length)) { + this.features.setData(features) + return; + } + + if (this.upper_left === undefined) { + this.upper_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2, this, this.options) + this.upper_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2, this, this.options) + this.lower_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2 + 1, this, this.options) + this.lower_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2 + 1, this, this.options) + } + + const ulf = [] + const urf = [] + const llf = [] + const lrf = [] + const overlapsboundary = [] + + for (const feature of features) { + const bbox = BBox.get(feature.feature) + if (this.options.minZoomLevel === undefined) { + + + if (bbox.isContainedIn(this.upper_left.bbox)) { + ulf.push(feature) + } else if (bbox.isContainedIn(this.upper_right.bbox)) { + urf.push(feature) + } else if (bbox.isContainedIn(this.lower_left.bbox)) { + llf.push(feature) + } else if (bbox.isContainedIn(this.lower_right.bbox)) { + lrf.push(feature) + } else { + overlapsboundary.push(feature) + } + } else { + // We duplicate a feature on a boundary into every tile as we need to get to the minZoomLevel + if (bbox.overlapsWith(this.upper_left.bbox)) { + ulf.push(feature) + } + if (bbox.overlapsWith(this.upper_right.bbox)) { + urf.push(feature) + } + if (bbox.overlapsWith(this.lower_left.bbox)) { + llf.push(feature) + } + if (bbox.overlapsWith(this.lower_right.bbox)) { + lrf.push(feature) + } + } + } + this.upper_left.addFeatures(ulf) + this.upper_right.addFeatures(urf) + this.lower_left.addFeatures(llf) + this.lower_right.addFeatures(lrf) + this.features.setData(overlapsboundary) + + } +} + +export interface TiledFeatureSourceOptions { + readonly maxFeatureCount?: number, + readonly maxZoomLevel?: number, + readonly minZoomLevel?: number, + readonly registerTile?: (tile: TiledFeatureSource & Tiled) => void, + readonly layer?: FilteredLayer +} \ No newline at end of file diff --git a/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts b/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts index 58c1fcaeb..7f3e5776e 100644 --- a/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/TiledFromLocalStorageSource.ts @@ -1,40 +1,102 @@ import FilteredLayer from "../../../Models/FilteredLayer"; -import {FeatureSourceForLayer} from "../FeatureSource"; +import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {UIEventSource} from "../../UIEventSource"; import Loc from "../../../Models/Loc"; -import GeoJsonSource from "../GeoJsonSource"; -import DynamicTileSource from "./DynamicTileSource"; +import TileHierarchy from "./TileHierarchy"; +import {Utils} from "../../../Utils"; +import LocalStorageSaverActor from "../Actors/LocalStorageSaverActor"; +import {BBox} from "../../GeoOperations"; + +export default class TiledFromLocalStorageSource implements TileHierarchy { + public loadedTiles: Map = new Map(); -export default class DynamicGeoJsonTileSource extends DynamicTileSource { constructor(layer: FilteredLayer, - registerLayer: (layer: FeatureSourceForLayer) => void, + handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void, state: { locationControl: UIEventSource leafletMap: any }) { - const source = layer.layerDef.source - if (source.geojsonZoomLevel === undefined) { - throw "Invalid layer: geojsonZoomLevel expected" - } - if (source.geojsonSource === undefined) { - throw "Invalid layer: geojsonSource expected" - } - super( - layer, - source.geojsonZoomLevel, - (xy) => { - const xyz: [number, number, number] = [xy[0], xy[1], source.geojsonZoomLevel] - const src = new GeoJsonSource( - layer, - xyz - ) - registerLayer(src) - return src - }, - state - ); + const prefix = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-" + // @ts-ignore + const indexes: number[] = Object.keys(localStorage) + .filter(key => { + return key.startsWith(prefix) && !key.endsWith("-time"); + }) + .map(key => { + return Number(key.substring(prefix.length)); + }) + + console.log("Avaible datasets in local storage:", indexes) + + const zLevels = indexes.map(i => i % 100) + const indexesSet = new Set(indexes) + const maxZoom = Math.max(...zLevels) + const minZoom = Math.min(...zLevels) + const self = this; + + const neededTiles = state.locationControl.map( + location => { + if (!layer.isDisplayed.data) { + // No need to download! - the layer is disabled + return undefined; + } + + if (location.zoom < layer.layerDef.minzoom) { + // No need to download! - the layer is disabled + return undefined; + } + + // Yup, this is cheating to just get the bounds here + const bounds = state.leafletMap.data?.getBounds() + if (bounds === undefined) { + // We'll retry later + return undefined + } + + const needed = [] + for (let z = minZoom; z <= maxZoom; z++) { + + const tileRange = Utils.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) + const neededZ = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(z, x, y)) + .filter(i => !self.loadedTiles.has(i) && indexesSet.has(i)) + needed.push(...neededZ) + } + + if (needed.length === 0) { + return undefined + } + return needed + } + , [layer.isDisplayed, state.leafletMap]).stabilized(50); + + neededTiles.addCallbackAndRun(t => console.log("Tiles to load from localstorage:", t)) + + neededTiles.addCallbackAndRunD(neededIndexes => { + for (const neededIndex of neededIndexes) { + // We load the features from localStorage + try { + const key = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-" + neededIndex + const data = localStorage.getItem(key) + const features = JSON.parse(data) + const src = { + layer: layer, + features: new UIEventSource<{ feature: any; freshness: Date }[]>(features), + name: "FromLocalStorage(" + key + ")", + tileIndex: neededIndex, + bbox: BBox.fromTile(...Utils.tile_from_index(neededIndex)) + } + handleFeatureSource(src, neededIndex) + self.loadedTiles.set(neededIndex, src) + } catch (e) { + console.error("Could not load data tile from local storage due to", e) + } + } + + + }) } + } \ No newline at end of file diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 81cc3d066..9fde92799 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -1,4 +1,5 @@ import * as turf from '@turf/turf' +import {Utils} from "../Utils"; export class GeoOperations { @@ -184,6 +185,44 @@ export class GeoOperations { static lengthInMeters(feature: any) { return turf.length(feature) * 1000 } + + static buffer(feature: any, bufferSizeInMeter: number){ + return turf.buffer(feature, bufferSizeInMeter/1000, { + units: 'kilometers' + }) + } + + static bbox(feature: any){ + const [lon, lat, lon0, lat0] = turf.bbox(feature) + return { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + lon, + lat + ], + [ + lon0, + lat + ], + [ + lon0, + lat0 + ], + [ + lon, + lat0 + ], + [ + lon, + lat + ], + ] + } + } + } /** * Generates the closest point on a way from a given point @@ -340,6 +379,7 @@ export class BBox { readonly maxLon: number; readonly minLat: number; readonly minLon: number; + static global: BBox = new BBox([[-180,-90],[180,90]]); constructor(coordinates) { this.maxLat = Number.MIN_VALUE; @@ -361,12 +401,11 @@ export class BBox { return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]]) } - static get(feature) { + static get(feature): BBox { if (feature.bbox?.overlapsWith === undefined) { const turfBbox: number[] = turf.bbox(feature) feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]); } - return feature.bbox; } @@ -407,4 +446,23 @@ export class BBox { } } + static fromTile(z: number, x: number, y: number) { + return new BBox( Utils.tile_bounds_lon_lat(z, x, y)) + } + + getEast() { + return this.maxLon + } + + getNorth() { + return this.maxLat + } + + getWest() { + return this.minLon + } + + getSouth() { + return this.minLat + } } \ No newline at end of file diff --git a/Logic/ImageProviders/Mapillary.ts b/Logic/ImageProviders/Mapillary.ts index 72a593d29..3f992dbce 100644 --- a/Logic/ImageProviders/Mapillary.ts +++ b/Logic/ImageProviders/Mapillary.ts @@ -32,7 +32,6 @@ export class Mapillary extends ImageAttributionSource { } const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/) - console.log("Mapview matched ", value, mapview) if(mapview !== null){ const key = mapview[1] return {key:key, isApiv4: !isNaN(Number(key))}; diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index 1f614cfec..de1731447 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -1,15 +1,9 @@ import SimpleMetaTagger from "./SimpleMetaTagger"; -import {ExtraFunction} from "./ExtraFunction"; -import {Relation} from "./Osm/ExtractRelations"; +import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction"; import {UIEventSource} from "./UIEventSource"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -interface Params { - featuresPerLayer: Map, - memberships: Map -} - /** * Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... * @@ -22,13 +16,12 @@ export default class MetaTagging { private static readonly stopErrorOutputAt = 10; /** - * An actor which adds metatags on every feature in the given object - * The features are a list of geojson-features, with a "properties"-field and geometry + * This method (re)calculates all metatags and calculated tags on every given object. + * The given features should be part of the given layer */ static addMetatags(features: { feature: any; freshness: Date }[], - allKnownFeatures: UIEventSource<{ feature: any; freshness: Date }[]>, - relations: Map, - layers: LayerConfig[], + params: ExtraFuncParams, + layer: LayerConfig, includeDates = true) { if (features === undefined || features.length === 0) { @@ -44,66 +37,39 @@ export default class MetaTagging { metatag.addMetaTags(features); } catch (e) { console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e) - } } // The functions - per layer - which add the new keys - const layerFuncs = new Map void)>(); - for (const layer of layers) { - layerFuncs.set(layer.id, this.createRetaggingFunc(layer)); - } - - allKnownFeatures.addCallbackAndRunD(newFeatures => { - - const featuresPerLayer = new Map(); - const allFeatures = Array.from(new Set(features.concat(newFeatures))) - for (const feature of allFeatures) { - - const key = feature.feature._matching_layer_id; - if (!featuresPerLayer.has(key)) { - featuresPerLayer.set(key, []) - } - featuresPerLayer.get(key).push(feature.feature) - } + const layerFuncs = this.createRetaggingFunc(layer) + if (layerFuncs !== undefined) { for (const feature of features) { - // @ts-ignore - const key = feature.feature._matching_layer_id; - const f = layerFuncs.get(key); - if (f === undefined) { - continue; - } try { - f({featuresPerLayer: featuresPerLayer, memberships: relations}, feature.feature) + layerFuncs(params, feature.feature) } catch (e) { console.error(e) } - } - - - }) - - + } } private static createRetaggingFunc(layer: LayerConfig): - ((params: Params, feature: any) => void) { + ((params: ExtraFuncParams, feature: any) => void) { const calculatedTags: [string, string][] = layer.calculatedTags; if (calculatedTags === undefined) { return undefined; } - const functions: ((params: Params, feature: any) => void)[] = []; + const functions: ((params: ExtraFuncParams, feature: any) => void)[] = []; for (const entry of calculatedTags) { const key = entry[0] const code = entry[1]; if (code === undefined) { continue; } - + const func = new Function("feat", "return " + code + ";"); try { @@ -145,14 +111,13 @@ export default class MetaTagging { console.error("Could not create a dynamic function: ", e) } } - return (params: Params, feature) => { + return (params: ExtraFuncParams, feature) => { const tags = feature.properties if (tags === undefined) { return; } - const relations = params.memberships?.get(feature.properties.id) ?? [] - ExtraFunction.FullPatchFeature(params.featuresPerLayer, relations, feature); + ExtraFunction.FullPatchFeature(params, feature); try { for (const f of functions) { f(params, feature); diff --git a/Logic/Osm/Actions/ChangeDescription.ts b/Logic/Osm/Actions/ChangeDescription.ts index f91d794fa..575c114f7 100644 --- a/Logic/Osm/Actions/ChangeDescription.ts +++ b/Logic/Osm/Actions/ChangeDescription.ts @@ -1,15 +1,30 @@ +/** + * Represents a single change to an object + */ export interface ChangeDescription { + /** + * Identifier of the object + */ type: "node" | "way" | "relation", /** - * Negative for a new objects + * Identifier of the object + * Negative for new objects */ id: number, - /* - v = "" or v = undefined to erase this tag - */ + + /** + * All changes to tags + * v = "" or v = undefined to erase this tag + */ tags?: { k: string, v: string }[], + /** + * A change to the geometry: + * 1) Change of node location + * 2) Change of way geometry + * 3) Change of relation members (untested) + */ changes?: { lat: number, lon: number diff --git a/Logic/Osm/Geocoding.ts b/Logic/Osm/Geocoding.ts index 44915d4e1..55dd99681 100644 --- a/Logic/Osm/Geocoding.ts +++ b/Logic/Osm/Geocoding.ts @@ -11,7 +11,7 @@ export class Geocoding { osm_type: string, osm_id: string }[]) => void), onFail: (() => void)) { - const b = State.state.leafletMap.data.getBounds(); + const b = State.state.currentBounds.data; const url = Geocoding.host + "format=json&limit=1&viewbox=" + `${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` + "&accept-language=nl&q=" + query; diff --git a/Logic/Osm/Overpass.ts b/Logic/Osm/Overpass.ts index f6ef4c83e..2f82889e3 100644 --- a/Logic/Osm/Overpass.ts +++ b/Logic/Osm/Overpass.ts @@ -1,7 +1,7 @@ import * as OsmToGeoJson from "osmtogeojson"; import Bounds from "../../Models/Bounds"; import {TagsFilter} from "../Tags/TagsFilter"; -import ExtractRelations from "./ExtractRelations"; +import RelationsTracker from "./RelationsTracker"; import {Utils} from "../../Utils"; import {UIEventSource} from "../UIEventSource"; @@ -15,16 +15,20 @@ export class Overpass { private readonly _timeout: UIEventSource; private readonly _extraScripts: string[]; private _includeMeta: boolean; - + private _relationTracker: RelationsTracker; + + constructor(filter: TagsFilter, extraScripts: string[], interpreterUrl: UIEventSource, timeout: UIEventSource, + relationTracker: RelationsTracker, includeMeta = true) { this._timeout = timeout; this._interpreterUrl = interpreterUrl; this._filter = filter this._extraScripts = extraScripts; this._includeMeta = includeMeta; + this._relationTracker = relationTracker } queryGeoJson(bounds: Bounds, continuation: ((any, date: Date) => void), onFail: ((reason) => void)): void { @@ -35,6 +39,7 @@ export class Overpass { console.log("Using testing URL") query = Overpass.testUrl; } + const self = this; Utils.downloadJson(query) .then(json => { if (json.elements === [] && ((json.remarks ?? json.remark).indexOf("runtime error") >= 0)) { @@ -44,13 +49,15 @@ export class Overpass { } - ExtractRelations.RegisterRelations(json) + self._relationTracker.RegisterRelations(json) // @ts-ignore const geojson = OsmToGeoJson.default(json); const osmTime = new Date(json.osm3s.timestamp_osm_base); continuation(geojson, osmTime); - }).catch(onFail) + }).catch(e => { + onFail(e); + }) } buildQuery(bbox: string): string { diff --git a/Logic/Osm/RelationsTracker.ts b/Logic/Osm/RelationsTracker.ts index 735b49512..f0528e77d 100644 --- a/Logic/Osm/RelationsTracker.ts +++ b/Logic/Osm/RelationsTracker.ts @@ -1,4 +1,5 @@ import State from "../../State"; +import {UIEventSource} from "../UIEventSource"; export interface Relation { id: number, @@ -13,11 +14,15 @@ export interface Relation { properties: any } -export default class ExtractRelations { +export default class RelationsTracker { - public static RegisterRelations(overpassJson: any): void { - const memberships = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(overpassJson)) - State.state.knownRelations.setData(memberships) + public knownRelations = new UIEventSource>(new Map(), "Relation memberships"); + + constructor() { + } + + public RegisterRelations(overpassJson: any): void { + this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson)) } /** @@ -25,7 +30,7 @@ export default class ExtractRelations { * @param overpassJson * @constructor */ - public static GetRelationElements(overpassJson: any): Relation[] { + private static GetRelationElements(overpassJson: any): Relation[] { const relations = overpassJson.elements .filter(element => element.type === "relation" && element.tags.type !== "multipolygon") for (const relation of relations) { @@ -39,12 +44,11 @@ export default class ExtractRelations { * @param relations * @constructor */ - public static BuildMembershipTable(relations: Relation[]): Map { - const memberships = new Map() - + private UpdateMembershipTable(relations: Relation[]): void { + const memberships = this.knownRelations.data + let changed = false; for (const relation of relations) { for (const member of relation.members) { - const role = { role: member.role, relation: relation @@ -53,11 +57,21 @@ export default class ExtractRelations { if (!memberships.has(key)) { memberships.set(key, []) } - memberships.get(key).push(role) + const knownRelations = memberships.get(key) + + const alreadyExists = knownRelations.some(knownRole => { + return knownRole.role === role.role && knownRole.relation === role.relation + }) + if (!alreadyExists) { + knownRelations.push(role) + changed = true; + } } } + if (changed) { + this.knownRelations.ping() + } - return memberships } } \ No newline at end of file diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 7440a067a..6e8f3e0fc 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -9,6 +9,7 @@ import Combine from "../UI/Base/Combine"; import BaseUIElement from "../UI/BaseUIElement"; import Title from "../UI/Base/Title"; import {FixedUiElement} from "../UI/Base/FixedUiElement"; +import CountryCoder from "latlon2country/index"; const cardinalDirections = { @@ -20,7 +21,7 @@ const cardinalDirections = { export default class SimpleMetaTagger { - static coder: any; + private static coder: CountryCoder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); public static readonly objectMetaInfo = new SimpleMetaTagger( { keys: ["_last_edit:contributor", @@ -84,7 +85,7 @@ export default class SimpleMetaTagger { }, (feature => { const units = Utils.NoNull([].concat(...State.state?.layoutToUse?.data?.layers?.map(layer => layer.units ?? []))); - if(units.length == 0){ + if (units.length == 0) { return; } let rewritten = false; @@ -93,7 +94,7 @@ export default class SimpleMetaTagger { continue; } for (const unit of units) { - if(unit.appliesToKeys === undefined){ + if (unit.appliesToKeys === undefined) { console.error("The unit ", unit, "has no appliesToKey defined") continue } @@ -148,7 +149,7 @@ export default class SimpleMetaTagger { const lat = centerPoint.geometry.coordinates[1]; const lon = centerPoint.geometry.coordinates[0]; - SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => { + SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, (countries: string[]) => { try { const oldCountry = feature.properties["_country"]; feature.properties["_country"] = countries[0].trim().toLowerCase(); @@ -160,7 +161,7 @@ export default class SimpleMetaTagger { } catch (e) { console.warn(e) } - }); + }) } ) private static isOpen = new SimpleMetaTagger( @@ -426,11 +427,7 @@ export default class SimpleMetaTagger { } } - static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) { - SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, callback) - } - - static HelpText(): BaseUIElement { + public static HelpText(): BaseUIElement { const subElements: (string | BaseUIElement)[] = [ new Combine([ new Title("Metatags", 1), @@ -453,7 +450,7 @@ export default class SimpleMetaTagger { return new Combine(subElements).SetClass("flex-col") } - addMetaTags(features: { feature: any, freshness: Date }[]) { + public addMetaTags(features: { feature: any, freshness: Date }[]) { for (let i = 0; i < features.length; i++) { let feature = features[i]; this._f(feature.feature, i, feature.freshness); diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index d1c85f226..705c6174a 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -81,9 +81,12 @@ export class UIEventSource { return this; } - public addCallbackAndRun(callback: ((latestData: T) => void)): UIEventSource { - callback(this.data); - return this.addCallback(callback); + public addCallbackAndRun(callback: ((latestData: T) => (boolean | void | any))): UIEventSource { + const doDeleteCallback = callback(this.data); + if (!doDeleteCallback) { + this.addCallback(callback); + } + return this; } public setData(t: T): UIEventSource { diff --git a/Models/Constants.ts b/Models/Constants.ts index 539ab8c6f..0b62cc0cc 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.9.12"; + public static vNumber = "0.10.0"; // The user journey states thresholds when a new feature gets unlocked public static userJourney = { @@ -26,12 +26,6 @@ export default class Constants { */ static updateTimeoutSec: number = 30; - /** - * If zoom >= useOsmApiAt, then the OSM api will be used directly. - * If undefined, use overpass exclusively - */ - static useOsmApiAt = undefined; - private static isRetina(): boolean { if (Utils.runningFromConsole) { return; diff --git a/Models/ThemeConfig/Json/LayerConfigJson.ts b/Models/ThemeConfig/Json/LayerConfigJson.ts index 3068d25fd..f337b1a19 100644 --- a/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -59,10 +59,9 @@ export interface LayerConfigJson { * NOTE: the previous format was 'overpassTags: AndOrTagConfigJson | string', which is interpreted as a shorthand for source: {osmTags: "key=value"} * While still supported, this is considered deprecated */ - source: { osmTags: AndOrTagConfigJson | string } | - { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean } | - { osmTags: AndOrTagConfigJson | string, overpassScript: string } - + source: { osmTags: AndOrTagConfigJson | string, overpassScript?: string } | + { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean } + /** * * A list of extra tags to calculate, specified as "keyToAssignTo=javascript-expression". diff --git a/Models/ThemeConfig/LayoutConfig.ts b/Models/ThemeConfig/LayoutConfig.ts index b57b6bab3..112810112 100644 --- a/Models/ThemeConfig/LayoutConfig.ts +++ b/Models/ThemeConfig/LayoutConfig.ts @@ -246,14 +246,6 @@ export default class LayoutConfig { return icons } - public LayerIndex(): Map { - const index = new Map(); - for (const layer of this.layers) { - index.set(layer.id, layer) - } - return index; - } - /** * Replaces all the relative image-urls with a fixed image url * This is to fix loading from external sources diff --git a/Models/ThemeConfig/SourceConfig.ts b/Models/ThemeConfig/SourceConfig.ts index db9d21f4f..1d223bd2b 100644 --- a/Models/ThemeConfig/SourceConfig.ts +++ b/Models/ThemeConfig/SourceConfig.ts @@ -2,18 +2,18 @@ import {TagsFilter} from "../../Logic/Tags/TagsFilter"; export default class SourceConfig { - osmTags?: TagsFilter; - overpassScript?: string; - geojsonSource?: string; - geojsonZoomLevel?: number; - isOsmCacheLayer: boolean; + public readonly osmTags?: TagsFilter; + public readonly overpassScript?: string; + public readonly geojsonSource?: string; + public readonly geojsonZoomLevel?: number; + public readonly isOsmCacheLayer: boolean; constructor(params: { osmTags?: TagsFilter, overpassScript?: string, geojsonSource?: string, isOsmCache?: boolean, - geojsonSourceLevel?: number + geojsonSourceLevel?: number, }, context?: string) { let defined = 0; diff --git a/State.ts b/State.ts index 27e9a251f..8c08873cb 100644 --- a/State.ts +++ b/State.ts @@ -11,16 +11,14 @@ import InstalledThemes from "./Logic/Actors/InstalledThemes"; import BaseLayer from "./Models/BaseLayer"; import Loc from "./Models/Loc"; import Constants from "./Models/Constants"; - -import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource"; import TitleHandler from "./Logic/Actors/TitleHandler"; import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; -import {Relation} from "./Logic/Osm/ExtractRelations"; -import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; +import OsmApiFeatureSource from "./Logic/FeatureSource/Sources/OsmApiFeatureSource"; import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; import FilteredLayer from "./Models/FilteredLayer"; import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; +import {BBox} from "./Logic/GeoOperations"; /** * Contains the global state: a bunch of UI-event sources @@ -57,8 +55,6 @@ export default class State { public favouriteLayers: UIEventSource; - public layerUpdater: OverpassFeatureSource; - public osmApiFeatureSource: OsmApiFeatureSource; public filteredLayers: UIEventSource = new UIEventSource([], "filteredLayers"); @@ -71,12 +67,6 @@ export default class State { "Selected element" ); - /** - * Keeps track of relations: which way is part of which other way? - * Set by the overpass-updater; used in the metatagging - */ - public readonly knownRelations = new UIEventSource>(undefined, "Relation memberships"); - public readonly featureSwitchUserbadge: UIEventSource; public readonly featureSwitchSearch: UIEventSource; public readonly featureSwitchBackgroundSlection: UIEventSource; @@ -96,6 +86,7 @@ export default class State { public readonly featureSwitchExportAsPdf: UIEventSource; public readonly overpassUrl: UIEventSource; public readonly overpassTimeout: UIEventSource; + public readonly overpassMaxZoom: UIEventSource = new UIEventSource(undefined); public featurePipeline: FeaturePipeline; @@ -104,6 +95,12 @@ export default class State { * The map location: currently centered lat, lon and zoom */ public readonly locationControl = new UIEventSource(undefined, "locationControl"); + + /** + * The current visible extent of the screen + */ + public readonly currentBounds = new UIEventSource(undefined) + public backgroundLayer; public readonly backgroundLayerId: UIEventSource; @@ -398,7 +395,7 @@ export default class State { new ChangeToElementsActor(this.changes, this.allElements) - this.osmApiFeatureSource = new OsmApiFeatureSource(Constants.useOsmApiAt, this) + this.osmApiFeatureSource = new OsmApiFeatureSource(this) new PendingChangesUploader(this.changes, this.selectedElement); diff --git a/UI/Base/Img.ts b/UI/Base/Img.ts index 947536b05..876265b86 100644 --- a/UI/Base/Img.ts +++ b/UI/Base/Img.ts @@ -10,6 +10,9 @@ export default class Img extends BaseUIElement { fallbackImage?: string }) { super(); + if(src === undefined || src === "undefined"){ + throw "Undefined src for image" + } this._src = src; this._rawSvg = rawSvg; this._options = options; diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 0c8cf3600..26909a00e 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -1,208 +1,30 @@ import BaseUIElement from "../BaseUIElement"; -import * as L from "leaflet"; -import {Map} from "leaflet"; -import {UIEventSource} from "../../Logic/UIEventSource"; import Loc from "../../Models/Loc"; import BaseLayer from "../../Models/BaseLayer"; -import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; -import {Utils} from "../../Utils"; +import {BBox} from "../../Logic/GeoOperations"; +import {UIEventSource} from "../../Logic/UIEventSource"; -export default class Minimap extends BaseUIElement { +export interface MinimapOptions { + background?: UIEventSource, + location?: UIEventSource, + bounds?: UIEventSource, + allowMoving?: boolean, + leafletOptions?: any, + attribution?: BaseUIElement | boolean, + onFullyLoaded?: (leaflet: L.Map) => void, + leafletMap?: UIEventSource, + lastClickLocation?: UIEventSource<{ lat: number, lon: number }> +} - private static _nextId = 0; - public readonly leafletMap: UIEventSource - private readonly _id: string; - private readonly _background: UIEventSource; - private readonly _location: UIEventSource; - private _isInited = false; - private _allowMoving: boolean; - private readonly _leafletoptions: any; - private readonly _onFullyLoaded: (leaflet: L.Map) => void - private readonly _attribution: BaseUIElement | boolean; - private readonly _lastClickLocation: UIEventSource<{ lat: number; lon: number }>; +export default class Minimap { + /** + * A stub implementation. The actual implementation is injected later on, but only in the browser. + * importing leaflet crashes node-ts, which is pretty annoying considering the fact that a lot of scripts use it + */ - constructor(options?: { - background?: UIEventSource, - location?: UIEventSource, - allowMoving?: boolean, - leafletOptions?: any, - attribution?: BaseUIElement | boolean, - onFullyLoaded?: (leaflet: L.Map) => void, - leafletMap?: UIEventSource, - lastClickLocation?: UIEventSource<{ lat: number, lon: number }> - } - ) { - super() - options = options ?? {} - this.leafletMap = options.leafletMap ?? new UIEventSource(undefined) - this._background = options?.background ?? new UIEventSource(AvailableBaseLayers.osmCarto) - this._location = options?.location ?? new UIEventSource({lat: 0, lon: 0, zoom: 1}) - this._id = "minimap" + Minimap._nextId; - this._allowMoving = options.allowMoving ?? true; - this._leafletoptions = options.leafletOptions ?? {} - this._onFullyLoaded = options.onFullyLoaded - this._attribution = options.attribution - this._lastClickLocation = options.lastClickLocation; - Minimap._nextId++ + /** + * Construct a minimap + */ + public static createMiniMap: (options: MinimapOptions) => BaseUIElement & { readonly leafletMap: UIEventSource } - } - - protected InnerConstructElement(): HTMLElement { - const div = document.createElement("div") - div.id = this._id; - div.style.height = "100%" - div.style.width = "100%" - div.style.minWidth = "40px" - div.style.minHeight = "40px" - div.style.position = "relative" - const wrapper = document.createElement("div") - wrapper.appendChild(div) - const self = this; - // @ts-ignore - const resizeObserver = new ResizeObserver(_ => { - self.InitMap(); - self.leafletMap?.data?.invalidateSize() - }); - - resizeObserver.observe(div); - return wrapper; - - } - - private InitMap() { - if (this._constructedHtmlElement === undefined) { - // This element isn't initialized yet - return; - } - - if (document.getElementById(this._id) === null) { - // not yet attached, we probably got some other event - return; - } - - if (this._isInited) { - return; - } - this._isInited = true; - const location = this._location; - const self = this; - let currentLayer = this._background.data.layer() - const options = { - center: <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0], - zoom: location.data?.zoom ?? 2, - layers: [currentLayer], - zoomControl: false, - attributionControl: this._attribution !== undefined, - dragging: this._allowMoving, - scrollWheelZoom: this._allowMoving, - doubleClickZoom: this._allowMoving, - keyboard: this._allowMoving, - touchZoom: this._allowMoving, - // Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving, - fadeAnimation: this._allowMoving, - } - - Utils.Merge(this._leafletoptions, options) - - const map = L.map(this._id, options); - if (self._onFullyLoaded !== undefined) { - - currentLayer.on("load", () => { - console.log("Fully loaded all tiles!") - self._onFullyLoaded(map) - }) - } - - // Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then - // We give a bit of leeway for people on the edges - // Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/ - - map.setMaxBounds( - [[-100, -200], [100, 200]] - ); - - if (this._attribution !== undefined) { - if (this._attribution === true) { - map.attributionControl.setPrefix(false) - } else { - map.attributionControl.setPrefix( - ""); - } - } - - this._background.addCallbackAndRun(layer => { - const newLayer = layer.layer() - if (currentLayer !== undefined) { - map.removeLayer(currentLayer); - } - currentLayer = newLayer; - if (self._onFullyLoaded !== undefined) { - - currentLayer.on("load", () => { - console.log("Fully loaded all tiles!") - self._onFullyLoaded(map) - }) - } - map.addLayer(newLayer); - map.setMaxZoom(layer.max_zoom ?? map.getMaxZoom()) - if (self._attribution !== true && self._attribution !== false) { - self._attribution?.AttachTo('leaflet-attribution') - } - - }) - - - let isRecursing = false; - map.on("moveend", function () { - if (isRecursing) { - return - } - if (map.getZoom() === location.data.zoom && - map.getCenter().lat === location.data.lat && - map.getCenter().lng === location.data.lon) { - return; - } - location.data.zoom = map.getZoom(); - location.data.lat = map.getCenter().lat; - location.data.lon = map.getCenter().lng; - isRecursing = true; - location.ping(); - isRecursing = false; // This is ugly, I know - }) - - - location.addCallback(loc => { - const mapLoc = map.getCenter() - const dlat = Math.abs(loc.lat - mapLoc[0]) - const dlon = Math.abs(loc.lon - mapLoc[1]) - - if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) { - return; - } - map.setView([loc.lat, loc.lon], loc.zoom) - }) - - location.map(loc => loc.zoom) - .addCallback(zoom => { - if (Math.abs(map.getZoom() - zoom) > 0.1) { - map.setZoom(zoom, {}); - } - }) - - - if (this._lastClickLocation) { - const lastClickLocation = this._lastClickLocation - map.on("click", function (e) { - // @ts-ignore - lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}) - }); - - map.on("contextmenu", function (e) { - // @ts-ignore - lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}); - }); - } - - this.leafletMap.setData(map) - } } \ No newline at end of file diff --git a/UI/Base/MinimapImplementation.ts b/UI/Base/MinimapImplementation.ts index e69de29bb..a55bc7e38 100644 --- a/UI/Base/MinimapImplementation.ts +++ b/UI/Base/MinimapImplementation.ts @@ -0,0 +1,215 @@ +import {Utils} from "../../Utils"; +import BaseUIElement from "../BaseUIElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Loc from "../../Models/Loc"; +import BaseLayer from "../../Models/BaseLayer"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; +import {BBox} from "../../Logic/GeoOperations"; +import * as L from "leaflet"; +import {Map} from "leaflet"; +import Minimap, {MinimapOptions} from "./Minimap"; + +export default class MinimapImplementation extends BaseUIElement { + private static _nextId = 0; + public readonly leafletMap: UIEventSource + private readonly _id: string; + private readonly _background: UIEventSource; + private readonly _location: UIEventSource; + private _isInited = false; + private _allowMoving: boolean; + private readonly _leafletoptions: any; + private readonly _onFullyLoaded: (leaflet: L.Map) => void + private readonly _attribution: BaseUIElement | boolean; + private readonly _lastClickLocation: UIEventSource<{ lat: number; lon: number }>; + private readonly _bounds: UIEventSource | undefined; + + private constructor(options: MinimapOptions) { + super() + options = options ?? {} + this.leafletMap = options.leafletMap ?? new UIEventSource(undefined) + this._background = options?.background ?? new UIEventSource(AvailableBaseLayers.osmCarto) + this._location = options?.location ?? new UIEventSource({lat: 0, lon: 0, zoom: 1}) + this._bounds = options?.bounds; + this._id = "minimap" + MinimapImplementation._nextId; + this._allowMoving = options.allowMoving ?? true; + this._leafletoptions = options.leafletOptions ?? {} + this._onFullyLoaded = options.onFullyLoaded + this._attribution = options.attribution + this._lastClickLocation = options.lastClickLocation; + MinimapImplementation._nextId++ + + } + + public static initialize() { + Minimap.createMiniMap = options => new MinimapImplementation(options) + } + + protected InnerConstructElement(): HTMLElement { + const div = document.createElement("div") + div.id = this._id; + div.style.height = "100%" + div.style.width = "100%" + div.style.minWidth = "40px" + div.style.minHeight = "40px" + div.style.position = "relative" + const wrapper = document.createElement("div") + wrapper.appendChild(div) + const self = this; + // @ts-ignore + const resizeObserver = new ResizeObserver(_ => { + self.InitMap(); + self.leafletMap?.data?.invalidateSize() + }); + + resizeObserver.observe(div); + return wrapper; + + } + + private InitMap() { + if (this._constructedHtmlElement === undefined) { + // This element isn't initialized yet + return; + } + + if (document.getElementById(this._id) === null) { + // not yet attached, we probably got some other event + return; + } + + if (this._isInited) { + return; + } + this._isInited = true; + const location = this._location; + const self = this; + let currentLayer = this._background.data.layer() + const options = { + center: <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0], + zoom: location.data?.zoom ?? 2, + layers: [currentLayer], + zoomControl: false, + attributionControl: this._attribution !== undefined, + dragging: this._allowMoving, + scrollWheelZoom: this._allowMoving, + doubleClickZoom: this._allowMoving, + keyboard: this._allowMoving, + touchZoom: this._allowMoving, + // Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving, + fadeAnimation: this._allowMoving, + } + + Utils.Merge(this._leafletoptions, options) + + const map = L.map(this._id, options); + if (self._onFullyLoaded !== undefined) { + + currentLayer.on("load", () => { + console.log("Fully loaded all tiles!") + self._onFullyLoaded(map) + }) + } + + // Users are not allowed to zoom to the 'copies' on the left and the right, stuff goes wrong then + // We give a bit of leeway for people on the edges + // Also see: https://www.reddit.com/r/openstreetmap/comments/ih4zzc/mapcomplete_a_new_easytouse_editor/g31ubyv/ + + map.setMaxBounds( + [[-100, -200], [100, 200]] + ); + + if (this._attribution !== undefined) { + if (this._attribution === true) { + map.attributionControl.setPrefix(false) + } else { + map.attributionControl.setPrefix( + ""); + } + } + + this._background.addCallbackAndRun(layer => { + const newLayer = layer.layer() + if (currentLayer !== undefined) { + map.removeLayer(currentLayer); + } + currentLayer = newLayer; + if (self._onFullyLoaded !== undefined) { + + currentLayer.on("load", () => { + console.log("Fully loaded all tiles!") + self._onFullyLoaded(map) + }) + } + map.addLayer(newLayer); + map.setMaxZoom(layer.max_zoom ?? map.getMaxZoom()) + if (self._attribution !== true && self._attribution !== false) { + self._attribution?.AttachTo('leaflet-attribution') + } + + }) + + + let isRecursing = false; + map.on("moveend", function () { + if (isRecursing) { + return + } + if (map.getZoom() === location.data.zoom && + map.getCenter().lat === location.data.lat && + map.getCenter().lng === location.data.lon) { + return; + } + location.data.zoom = map.getZoom(); + location.data.lat = map.getCenter().lat; + location.data.lon = map.getCenter().lng; + isRecursing = true; + location.ping(); + + if (self._bounds !== undefined) { + self._bounds.setData(BBox.fromLeafletBounds(map.getBounds())) + } + + + isRecursing = false; // This is ugly, I know + }) + + + location.addCallback(loc => { + const mapLoc = map.getCenter() + const dlat = Math.abs(loc.lat - mapLoc[0]) + const dlon = Math.abs(loc.lon - mapLoc[1]) + + if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) { + return; + } + map.setView([loc.lat, loc.lon], loc.zoom) + }) + + location.map(loc => loc.zoom) + .addCallback(zoom => { + if (Math.abs(map.getZoom() - zoom) > 0.1) { + map.setZoom(zoom, {}); + } + }) + + if (self._bounds !== undefined) { + self._bounds.setData(BBox.fromLeafletBounds(map.getBounds())) + } + + + if (this._lastClickLocation) { + const lastClickLocation = this._lastClickLocation + map.on("click", function (e) { + // @ts-ignore + lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}) + }); + + map.on("contextmenu", function (e) { + // @ts-ignore + lastClickLocation?.setData({lat: e.latlng.lat, lon: e.latlng.lng}); + }); + } + + this.leafletMap.setData(map) + } +} \ No newline at end of file diff --git a/UI/BigComponents/AllDownloads.ts b/UI/BigComponents/AllDownloads.ts index 353a4c38b..2170eb496 100644 --- a/UI/BigComponents/AllDownloads.ts +++ b/UI/BigComponents/AllDownloads.ts @@ -32,7 +32,7 @@ export default class AllDownloads extends ScrollableFullScreen { freeDivId: "belowmap", background: State.state.backgroundLayer, location: State.state.locationControl, - features: State.state.featurePipeline.features, + features: State.state.featurePipeline, layout: State.state.layoutToUse, }).isRunning.addCallbackAndRun(isRunning => isExporting.setData(isRunning)) } diff --git a/UI/BigComponents/Attribution.ts b/UI/BigComponents/Attribution.ts index 2d322a961..7e70fa367 100644 --- a/UI/BigComponents/Attribution.ts +++ b/UI/BigComponents/Attribution.ts @@ -5,19 +5,19 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import UserDetails from "../../Logic/Osm/OsmConnection"; import Constants from "../../Models/Constants"; import Loc from "../../Models/Loc"; -import * as L from "leaflet" import {VariableUiElement} from "../Base/VariableUIElement"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import {BBox} from "../../Logic/GeoOperations"; /** * The bottom right attribution panel in the leaflet map */ export default class Attribution extends Combine { - constructor(location: UIEventSource, + constructor(location: UIEventSource, userDetails: UIEventSource, layoutToUse: UIEventSource, - leafletMap: UIEventSource) { + currentBounds: UIEventSource) { const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true); const reportBug = new Link(Svg.bug_ui().SetClass("small-image"), "https://github.com/pietervdvn/MapComplete/issues", true); @@ -43,7 +43,7 @@ export default class Attribution extends Combine { if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) { return undefined; } - const bounds: any = leafletMap?.data?.getBounds(); + const bounds: any = currentBounds.data; if (bounds === undefined) { return undefined } @@ -55,7 +55,7 @@ export default class Attribution extends Combine { const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}` return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true); }, - [location, leafletMap] + [location, currentBounds] ) ) super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]); diff --git a/UI/BigComponents/AttributionPanel.ts b/UI/BigComponents/AttributionPanel.ts index 7a0bcef81..542326314 100644 --- a/UI/BigComponents/AttributionPanel.ts +++ b/UI/BigComponents/AttributionPanel.ts @@ -26,10 +26,13 @@ export default class AttributionPanel extends Combine { ((layoutToUse.data.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.data.maintainer}), layoutToUse.data.credits, "
", - new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.leafletMap), + new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.currentBounds), "
", new VariableUiElement(contributions.map(contributions => { + if(contributions === undefined){ + return "" + } const sorted = Array.from(contributions, ([name, value]) => ({ name, value diff --git a/UI/BigComponents/DownloadPanel.ts b/UI/BigComponents/DownloadPanel.ts index e291bf6b7..466b8d217 100644 --- a/UI/BigComponents/DownloadPanel.ts +++ b/UI/BigComponents/DownloadPanel.ts @@ -2,54 +2,113 @@ import {SubtleButton} from "../Base/SubtleButton"; import Svg from "../../Svg"; import Translations from "../i18n/Translations"; import State from "../../State"; -import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource"; import {Utils} from "../../Utils"; import Combine from "../Base/Combine"; import CheckBoxes from "../Input/Checkboxes"; -import {GeoOperations} from "../../Logic/GeoOperations"; +import {BBox, GeoOperations} from "../../Logic/GeoOperations"; import Toggle from "../Input/Toggle"; import Title from "../Base/Title"; +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import SimpleMetaTagger from "../../Logic/SimpleMetaTagger"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import {meta} from "@turf/turf"; export class DownloadPanel extends Toggle { + constructor() { + const state: { + featurePipeline: FeaturePipeline, + layoutToUse: UIEventSource, + currentBounds: UIEventSource + } = State.state + + const t = Translations.t.general.download - const somethingLoaded = State.state.featurePipeline.features.map(features => features.length > 0); + const name = State.state.layoutToUse.data.id; + const includeMetaToggle = new CheckBoxes([t.includeMetaData.Clone()]) const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0) + + const buttonGeoJson = new SubtleButton(Svg.floppy_ui(), new Combine([t.downloadGeojson.Clone().SetClass("font-bold"), t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col")) .onClick(() => { - const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data}) - const name = State.state.layoutToUse.data.id; - Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson), + const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) + Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, { mimetype: "application/vnd.geo+json" }); }) + const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine( [t.downloadCSV.Clone().SetClass("font-bold"), t.downloadCSVHelper.Clone()]).SetClass("flex flex-col")) .onClick(() => { - const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data}) + const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) const csv = GeoOperations.toCSV(geojson.features) - const name = State.state.layoutToUse.data.id; Utils.offerContentsAsDownloadableFile(csv, `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, { mimetype: "text/csv" }); - - }) + const downloadButtons = new Combine( - [new Title(t.title), buttonGeoJson, buttonCSV, includeMetaToggle, t.licenseInfo.Clone().SetClass("link-underline")]) + [new Title(t.title), + buttonGeoJson, + buttonCSV, + includeMetaToggle, + t.licenseInfo.Clone().SetClass("link-underline")]) .SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4") super( downloadButtons, t.noDataLoaded.Clone(), - somethingLoaded) + state.featurePipeline.somethingLoaded) + } + + private static getCleanGeoJson(state: { + featurePipeline: FeaturePipeline, + currentBounds: UIEventSource + }, includeMetaData: boolean) { + + const resultFeatures = [] + const featureList = state.featurePipeline.GetAllFeaturesWithin(state.currentBounds.data); + for (const tile of featureList) { + for (const feature of tile) { + const cleaned = { + type: feature.type, + geometry: feature.geometry, + properties: {...feature.properties} + } + + if (!includeMetaData) { + for (const key in cleaned.properties) { + if (key === "_lon" || key === "_lat") { + continue; + } + if (key.startsWith("_")) { + delete feature.properties[key] + } + } + } + + const datedKeys = [].concat(SimpleMetaTagger.metatags.filter(tagging => tagging.includesDates).map(tagging => tagging.keys)) + for (const key of datedKeys) { + delete feature.properties[key] + } + + resultFeatures.push(feature) + } + } + + return { + type:"FeatureCollection", + features: featureList + } + } } \ No newline at end of file diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index a73ac1ce5..eba8b60b3 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -15,6 +15,7 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen"; import BaseUIElement from "../BaseUIElement"; import Toggle from "../Input/Toggle"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import {Utils} from "../../Utils"; export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { @@ -62,9 +63,15 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown) const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)] + + const now = new Date() + const date = now.getFullYear()+"-"+Utils.TwoDigits(now.getMonth()+1)+"-"+Utils.TwoDigits(now.getDate()) + const osmcha_link = `https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%22${date}%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D` + tabsWithAboutMc.push({ header: Svg.help, - content: new Combine([Translations.t.general.aboutMapcomplete.Clone(), "
Version " + Constants.vNumber]) + content: new Combine([Translations.t.general.aboutMapcomplete.Clone() + .Subs({"osmcha_link": osmcha_link}), "
Version " + Constants.vNumber]) .SetClass("link-underline") } ); diff --git a/UI/BigComponents/ImportButton.ts b/UI/BigComponents/ImportButton.ts index 1d690bae6..f62b912fd 100644 --- a/UI/BigComponents/ImportButton.ts +++ b/UI/BigComponents/ImportButton.ts @@ -52,7 +52,7 @@ export default class ImportButton extends Toggle { const withLoadingCheck = new Toggle( t.stillLoading, new Combine([button, appliedTags]).SetClass("flex flex-col"), - State.state.layerUpdater.runningQuery + State.state.featurePipeline.runningQuery ) super(t.hasBeenImported, withLoadingCheck, isImported) } diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index db58abac6..b3e848d4d 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -9,18 +9,21 @@ import MapControlButton from "../MapControlButton"; import Svg from "../../Svg"; import AllDownloads from "./AllDownloads"; import FilterView from "./FilterView"; -import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; +import {BBox} from "../../Logic/GeoOperations"; +import Loc from "../../Models/Loc"; export default class LeftControls extends Combine { - constructor(featureSource: FeatureSource) { + constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource, locationControl: UIEventSource}) { const toggledCopyright = new ScrollableFullScreen( () => Translations.t.general.attribution.attributionTitle.Clone(), () => new AttributionPanel( State.state.layoutToUse, - new ContributorCount(featureSource).Contributors + new ContributorCount(state).Contributors ), undefined ); diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index 6a3729bc5..673f1d325 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -65,10 +65,6 @@ export default class SimpleAddUI extends Toggle { State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( newElementAction.newElementId )) - console.log("Did set selected element to", State.state.allElements.ContainingFeatures.get( - newElementAction.newElementId - )) - } const addUi = new VariableUiElement( @@ -104,7 +100,7 @@ export default class SimpleAddUI extends Toggle { new Toggle( Translations.t.general.add.stillLoading.Clone().SetClass("alert"), addUi, - State.state.layerUpdater.runningQuery + State.state.featurePipeline.runningQuery ), Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"), State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints) @@ -150,7 +146,6 @@ export default class SimpleAddUI extends Toggle { } const tags = TagUtils.KVtoProperties(preset.tags ?? []); - console.log("Opening precise input ", preset.preciseInput, "with tags", tags) preciseInput = new LocationInput({ mapBackground: backgroundLayer, centerLocation: locationSrc, @@ -215,10 +210,7 @@ export default class SimpleAddUI extends Toggle { const disableFiltersOrConfirm = new Toggle( openLayerOrConfirm, disableFilter, - preset.layerToAddTo.appliedFilters.map(filters => { - console.log("Current filters are ", filters) - return filters === undefined || filters.normalize().and.length === 0; - }) + preset.layerToAddTo.appliedFilters.map(filters => filters === undefined || filters.normalize().and.length === 0) ) diff --git a/UI/CenterMessageBox.ts b/UI/CenterMessageBox.ts index 62efc2ead..fe1ef5c65 100644 --- a/UI/CenterMessageBox.ts +++ b/UI/CenterMessageBox.ts @@ -6,7 +6,7 @@ export default class CenterMessageBox extends VariableUiElement { constructor() { const state = State.state; - const updater = State.state.layerUpdater; + const updater = State.state.featurePipeline; const t = Translations.t.centerMessage; const message = updater.runningQuery.map( isRunning => { diff --git a/UI/ExportPDF.ts b/UI/ExportPDF.ts index aea7561b0..f1a113daa 100644 --- a/UI/ExportPDF.ts +++ b/UI/ExportPDF.ts @@ -1,3 +1,19 @@ + + +import jsPDF from "jspdf"; +import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter"; +import {UIEventSource} from "../Logic/UIEventSource"; +import Minimap from "./Base/Minimap"; +import Loc from "../Models/Loc"; +import {BBox} from "../Logic/GeoOperations"; +import BaseLayer from "../Models/BaseLayer"; +import {FixedUiElement} from "./Base/FixedUiElement"; +import Translations from "./i18n/Translations"; +import State from "../State"; +import Constants from "../Models/Constants"; +import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; +import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; +import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; /** * Creates screenshoter to take png screenshot * Creates jspdf and downloads it @@ -8,21 +24,6 @@ * - add new layout in "PDFLayout" * -> in there are more instructions */ - -import jsPDF from "jspdf"; -import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter"; -import {UIEventSource} from "../Logic/UIEventSource"; -import Minimap from "./Base/Minimap"; -import Loc from "../Models/Loc"; -import {BBox} from "../Logic/GeoOperations"; -import ShowDataLayer from "./ShowDataLayer"; -import BaseLayer from "../Models/BaseLayer"; -import {FixedUiElement} from "./Base/FixedUiElement"; -import Translations from "./i18n/Translations"; -import State from "../State"; -import Constants from "../Models/Constants"; -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; - export default class ExportPDF { // dimensions of the map in milimeter public isRunning = new UIEventSource(true) @@ -39,7 +40,7 @@ export default class ExportPDF { freeDivId: string, location: UIEventSource, background?: UIEventSource - features: UIEventSource<{ feature: any }[]>, + features: FeaturePipeline, layout: UIEventSource } ) { @@ -57,7 +58,7 @@ export default class ExportPDF { zoom: l.zoom + 1 } - const minimap = new Minimap({ + const minimap = Minimap.createMiniMap({ location: new UIEventSource(loc), // We remove the link between the old and the new UI-event source as moving the map while the export is running fucks up the screenshot background: options.background, allowMoving: false, @@ -83,24 +84,21 @@ export default class ExportPDF { minimap.AttachTo(options.freeDivId) // Next: we prepare the features. Only fully contained features are shown - const bounded = options.features.map(feats => { - - const leaflet = minimap.leafletMap.data; - if (leaflet === undefined) { - return feats - } + minimap.leafletMap .addCallbackAndRunD(leaflet => { const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2)) - return feats.filter(f => BBox.get(f.feature).isContainedIn(bounds)) - - }, [minimap.leafletMap]) - - // Add the features to the minimap - new ShowDataLayer( - bounded, - minimap.leafletMap, - options.layout, - false - ) + options.features.GetTilesPerLayerWithin(bounds, tile => { + console.log("REndering", tile.name) + new ShowDataLayer( + { + features: tile, + leafletMap: minimap.leafletMap, + layerToShow: tile.layer.layerDef, + enablePopups: false + } + ) + }) + + }) } diff --git a/UI/Image/AttributedImage.ts b/UI/Image/AttributedImage.ts index 69835e105..7614077c5 100644 --- a/UI/Image/AttributedImage.ts +++ b/UI/Image/AttributedImage.ts @@ -4,6 +4,7 @@ import Img from "../Base/Img"; import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource"; import BaseUIElement from "../BaseUIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; +import Loading from "../Base/Loading"; export class AttributedImage extends Combine { @@ -16,8 +17,13 @@ export class AttributedImage extends Combine { img = new Img(urlSource); attr = new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon()) } else { - img = new VariableUiElement(preparedUrl.map(url => new Img(url, false, {fallbackImage: './assets/svg/blocked.svg'}))) - attr = new VariableUiElement(preparedUrl.map(url => new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon()))) + img = new VariableUiElement(preparedUrl.map(url => { + if(url === undefined){ + return new Loading() + } + return new Img(url, false, {fallbackImage: './assets/svg/blocked.svg'}); + })) + attr = new VariableUiElement(preparedUrl.map(_ => new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon()))) } diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts index 6c6b15948..108065b68 100644 --- a/UI/Input/DirectionInput.ts +++ b/UI/Input/DirectionInput.ts @@ -6,13 +6,13 @@ import BaseUIElement from "../BaseUIElement"; import {FixedUiElement} from "../Base/FixedUiElement"; import {Utils} from "../../Utils"; import Loc from "../../Models/Loc"; +import Minimap from "../Base/Minimap"; /** * Selects a direction in degrees */ export default class DirectionInput extends InputElement { - public static constructMinimap: ((any) => BaseUIElement); public readonly IsSelected: UIEventSource = new UIEventSource(false); private readonly _location: UIEventSource; private readonly value: UIEventSource; @@ -40,7 +40,7 @@ export default class DirectionInput extends InputElement { let map: BaseUIElement = new FixedUiElement("") if (!Utils.runningFromConsole) { - map = DirectionInput.constructMinimap({ + map = Minimap.createMiniMap({ background: this.background, allowMoving: false, location: this._location diff --git a/UI/Input/LengthInput.ts b/UI/Input/LengthInput.ts index 3162f5a87..4eb39d7e9 100644 --- a/UI/Input/LengthInput.ts +++ b/UI/Input/LengthInput.ts @@ -5,7 +5,7 @@ import Svg from "../../Svg"; import {Utils} from "../../Utils"; import Loc from "../../Models/Loc"; import {GeoOperations} from "../../Logic/GeoOperations"; -import DirectionInput from "./DirectionInput"; +import Minimap from "../Base/Minimap"; /** @@ -41,7 +41,7 @@ export default class LengthInput extends InputElement { // @ts-ignore let map = undefined if (!Utils.runningFromConsole) { - map = DirectionInput.constructMinimap({ + map = Minimap.createMiniMap({ background: this.background, allowMoving: false, location: this._location, diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index 875fce9cb..a18c887f8 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -8,35 +8,31 @@ import Svg from "../../Svg"; import State from "../../State"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import {GeoOperations} from "../../Logic/GeoOperations"; -import ShowDataLayer from "../ShowDataLayer"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; import * as L from "leaflet"; +import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; export default class LocationInput extends InputElement { - private static readonly matchLayout = new UIEventSource(new LayoutConfig({ - description: "Matchpoint style", - icon: "./assets/svg/crosshair-empty.svg", - id: "matchpoint", - language: ["en"], - layers: [{ + private static readonly matchLayer = new LayerConfig( + { id: "matchpoint", source: { osmTags: {and: []} }, icon: "./assets/svg/crosshair-empty.svg" - }], - maintainer: "MapComplete", - startLat: 0, - startLon: 0, - startZoom: 0, - title: "Location input", - version: "0" - - })); + }, "matchpoint icon", true + ) + IsSelected: UIEventSource = new UIEventSource(false); public readonly snappedOnto: UIEventSource = new UIEventSource(undefined) private _centerLocation: UIEventSource; private readonly mapBackground: UIEventSource; + /** + * The features to which the input should be snapped + * @private + */ private readonly _snapTo: UIEventSource<{ feature: any }[]> private readonly _value: UIEventSource private readonly _snappedPoint: UIEventSource @@ -143,7 +139,7 @@ export default class LocationInput extends InputElement { protected InnerConstructElement(): HTMLElement { try { const clickLocation = new UIEventSource(undefined); - const map = new Minimap( + const map = Minimap.createMiniMap( { location: this._centerLocation, background: this.mapBackground, @@ -198,7 +194,6 @@ export default class LocationInput extends InputElement { }) if (this._snapTo !== undefined) { - new ShowDataLayer(this._snapTo, map.leafletMap, State.state.layoutToUse, false, false) const matchPoint = this._snappedPoint.map(loc => { if (loc === undefined) { @@ -207,16 +202,25 @@ export default class LocationInput extends InputElement { return [{feature: loc}]; }) if (this._snapTo) { - let layout = LocationInput.matchLayout - if (this._snappedPointTags !== undefined) { - layout = State.state.layoutToUse + if (this._snappedPointTags === undefined) { + // No special tags - we show a default crosshair + new ShowDataLayer({ + features: new StaticFeatureSource(matchPoint), + enablePopups: false, + zoomToFeatures: false, + leafletMap: map.leafletMap, + layerToShow: LocationInput.matchLayer + }) + }else{ + new ShowDataMultiLayer({ + features: new StaticFeatureSource(matchPoint), + enablePopups: false, + zoomToFeatures: false, + leafletMap: map.leafletMap, + layers: State.state.filteredLayers + } + ) } - new ShowDataLayer( - matchPoint, - map.leafletMap, - layout, - false, false - ) } } diff --git a/UI/Popup/SplitRoadWizard.ts b/UI/Popup/SplitRoadWizard.ts index 9185cf24c..ca0980592 100644 --- a/UI/Popup/SplitRoadWizard.ts +++ b/UI/Popup/SplitRoadWizard.ts @@ -4,7 +4,7 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import {SubtleButton} from "../Base/SubtleButton"; import Minimap from "../Base/Minimap"; import State from "../../State"; -import ShowDataLayer from "../ShowDataLayer"; +import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; import {GeoOperations} from "../../Logic/GeoOperations"; import {LeafletMouseEvent} from "leaflet"; import Combine from "../Base/Combine"; @@ -13,10 +13,16 @@ import Translations from "../i18n/Translations"; import SplitAction from "../../Logic/Osm/Actions/SplitAction"; import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; import Title from "../Base/Title"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; +import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; export default class SplitRoadWizard extends Toggle { - private static splitLayout = new UIEventSource(SplitRoadWizard.GetSplitLayout()) + private static splitLayerStyling = new LayerConfig({ + id: "splitpositions", + source: {osmTags: "_cutposition=yes"}, + icon: "./assets/svg/plus.svg" + }, "(BUILTIN) SplitRoadWizard.ts", true) /** * A UI Element used for splitting roads @@ -36,7 +42,7 @@ export default class SplitRoadWizard extends Toggle { const splitClicked = new UIEventSource(false); // Minimap on which you can select the points to be splitted - const miniMap = new Minimap({background: State.state.backgroundLayer, allowMoving: false}); + const miniMap = Minimap.createMiniMap({background: State.state.backgroundLayer, allowMoving: false}); miniMap.SetStyle("width: 100%; height: 24rem;"); // Define how a cut is displayed on the map @@ -45,8 +51,20 @@ export default class SplitRoadWizard extends Toggle { const roadElement = State.state.allElements.ContainingFeatures.get(id) const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]); // Datalayer displaying the road and the cut points (if any) - new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true); - new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false) + new ShowDataMultiLayer({ + features: new StaticFeatureSource(roadEventSource, true), + layers: State.state.filteredLayers, + leafletMap: miniMap.leafletMap, + enablePopups: false, + zoomToFeatures: true + }) + new ShowDataLayer({ + features: new StaticFeatureSource(splitPoints, true), + leafletMap: miniMap.leafletMap, + zoomToFeatures: false, + enablePopups: false, + layerToShow: SplitRoadWizard.splitLayerStyling + }) /** * Handles a click on the overleaf map. @@ -135,21 +153,4 @@ export default class SplitRoadWizard extends Toggle { const confirm = new Toggle(mapView, splitToggle, splitClicked); super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit) } - - private static GetSplitLayout(): LayoutConfig { - return new LayoutConfig({ - maintainer: "mapcomplete", - language: ["en"], - startLon: 0, - startLat: 0, - description: "Split points visualisations - built in at SplitRoadWizard.ts", - icon: "", startZoom: 0, - title: "Split locations", - version: "", - - id: "splitpositions", - layers: [{id: "splitpositions", source: {osmTags: "_cutposition=yes"}, icon: "./assets/svg/plus.svg"}] - }, true, "(BUILTIN) SplitRoadWizard.ts") - - } } \ No newline at end of file diff --git a/UI/ShowDataLayer/ShowDataLayer.ts b/UI/ShowDataLayer/ShowDataLayer.ts index 661e51d07..927a1008d 100644 --- a/UI/ShowDataLayer/ShowDataLayer.ts +++ b/UI/ShowDataLayer/ShowDataLayer.ts @@ -1,19 +1,11 @@ /** * The data layer shows all the given geojson elements with the appropriate icon etc */ -import {UIEventSource} from "../Logic/UIEventSource"; -import * as L from "leaflet" -import State from "../State"; -import FeatureInfoBox from "./Popup/FeatureInfoBox"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import FeatureSource from "../Logic/FeatureSource/FeatureSource"; - -export interface ShowDataLayerOptions { - features: FeatureSource, - leafletMap: UIEventSource, - enablePopups?: true | boolean, - zoomToFeatures? : false | boolean, -} +import {UIEventSource} from "../../Logic/UIEventSource"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import FeatureInfoBox from "../Popup/FeatureInfoBox"; +import State from "../../State"; +import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; export default class ShowDataLayer { diff --git a/UI/ShowDataLayer/ShowDataLayerOptions.ts b/UI/ShowDataLayer/ShowDataLayerOptions.ts index e69de29bb..349472351 100644 --- a/UI/ShowDataLayer/ShowDataLayerOptions.ts +++ b/UI/ShowDataLayer/ShowDataLayerOptions.ts @@ -0,0 +1,9 @@ +import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; +import {UIEventSource} from "../../Logic/UIEventSource"; + +export interface ShowDataLayerOptions { + features: FeatureSource, + leafletMap: UIEventSource, + enablePopups?: true | boolean, + zoomToFeatures?: false | boolean, +} \ No newline at end of file diff --git a/UI/ShowDataLayer/ShowDataMultiLayer.ts b/UI/ShowDataLayer/ShowDataMultiLayer.ts index 5bff575cb..924274586 100644 --- a/UI/ShowDataLayer/ShowDataMultiLayer.ts +++ b/UI/ShowDataLayer/ShowDataMultiLayer.ts @@ -1,11 +1,12 @@ -import {UIEventSource} from "../Logic/UIEventSource"; -import FilteredLayer from "../Models/FilteredLayer"; -import ShowDataLayer, {ShowDataLayerOptions} from "./ShowDataLayer/ShowDataLayer"; -import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; - /** * SHows geojson on the given leaflet map, but attempts to figure out the correct layer first */ +import {UIEventSource} from "../../Logic/UIEventSource"; +import ShowDataLayer from "./ShowDataLayer"; +import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; +import FilteredLayer from "../../Models/FilteredLayer"; +import {ShowDataLayerOptions} from "./ShowDataLayerOptions"; + export default class ShowDataMultiLayer { constructor(options: ShowDataLayerOptions & { layers: UIEventSource }) { diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 6af9e5a57..7814cbbdc 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -5,7 +5,6 @@ import {ImageCarousel} from "./Image/ImageCarousel"; import Combine from "./Base/Combine"; import {FixedUiElement} from "./Base/FixedUiElement"; import {ImageUploadFlow} from "./Image/ImageUploadFlow"; - import ShareButton from "./BigComponents/ShareButton"; import Svg from "../Svg"; import ReviewElement from "./Reviews/ReviewElement"; @@ -13,7 +12,6 @@ import MangroveReviews from "../Logic/Web/MangroveReviews"; import Translations from "./i18n/Translations"; import ReviewForm from "./Reviews/ReviewForm"; import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; - import State from "../State"; import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; import BaseUIElement from "./BaseUIElement"; @@ -26,6 +24,9 @@ import BaseLayer from "../Models/BaseLayer"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import ImportButton from "./BigComponents/ImportButton"; import {Tag} from "../Logic/Tags/Tag"; +import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; +import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer"; +import Minimap from "./Base/Minimap"; export interface SpecialVisualization { funcName: string, @@ -37,14 +38,6 @@ export interface SpecialVisualization { export default class SpecialVisualizations { - - static constructMiniMap: (options?: { - background?: UIEventSource, - location?: UIEventSource, - allowMoving?: boolean, - leafletOptions?: any - }) => BaseUIElement; - static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource, layoutToUse: UIEventSource, enablePopups?: boolean, zoomToFeatures?: boolean) => any; public static specialVisualizations: SpecialVisualization[] = [ { @@ -153,7 +146,7 @@ export default class SpecialVisualizations { lon: Number(properties._lon), zoom: zoom }) - const minimap = SpecialVisualizations.constructMiniMap( + const minimap = Minimap.createMiniMap( { background: state.backgroundLayer, location: locationSource, @@ -169,12 +162,14 @@ export default class SpecialVisualizations { } }) - SpecialVisualizations.constructShowDataLayer( - featuresToShow, - minimap["leafletMap"], - State.state.layoutToUse, - false, - true + new ShowDataMultiLayer( + { + leafletMap: minimap["leafletMap"], + enablePopups : false, + zoomToFeatures: true, + layers: State.state.filteredLayers, + features: new StaticFeatureSource(featuresToShow, true) + } ) diff --git a/Utils.ts b/Utils.ts index 4cfc52520..606dccb32 100644 --- a/Utils.ts +++ b/Utils.ts @@ -245,7 +245,6 @@ export class Utils { } dict.set(k, v()); return dict.get(k); - } /** @@ -259,6 +258,26 @@ export class Utils { return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]] } + static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] { + return [[Utils.tile2long(x, z),Utils.tile2lat(y, z)], [Utils.tile2long(x + 1, z), Utils.tile2lat(y + 1, z)]] + } + + static tile_index(z: number, x: number, y: number):number{ + return ((x * (2 << z)) + y) * 100 + z + } + + /** + * Given a tile index number, returns [z, x, y] + * @param index + * @returns 'zxy' + */ + static tile_from_index(index: number) : [number, number, number]{ + const z = index % 100; + const factor = 2 << z + index = Math.floor(index / 100) + return [z, Math.floor(index / factor), index % factor] + } + /** * Return x, y of the tile containing (lat, lon) on the given zoom level */ @@ -422,13 +441,6 @@ export class Utils { return bestColor ?? hex; } - public static setDefaults(options, defaults) { - for (let key in defaults) { - if (!(key in options)) options[key] = defaults[key]; - } - return options; - } - private static tile2long(x, z) { return (x / Math.pow(2, z) * 360 - 180); } diff --git a/assets/layers/drinking_water/drinking_water.json b/assets/layers/drinking_water/drinking_water.json index 7663f584b..f6acd494b 100644 --- a/assets/layers/drinking_water/drinking_water.json +++ b/assets/layers/drinking_water/drinking_water.json @@ -48,7 +48,7 @@ } }, "calculatedTags": [ - "_closest_other_drinking_water_id=feat.closest('drinking_water').id", + "_closest_other_drinking_water_id=feat.closest('drinking_water')?.id", "_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')) * 1000)" ], "minzoom": 13, diff --git a/assets/layers/toilet/toilet.json b/assets/layers/toilet/toilet.json index e70dc2b78..fd1043c8a 100644 --- a/assets/layers/toilet/toilet.json +++ b/assets/layers/toilet/toilet.json @@ -401,5 +401,43 @@ } ] } + ], + "filter": [ + { + "options": [ + { + "question": { + "en": "Wheelchair accessible" + }, + "osmTags": "wheelchair=yes" + } + ] + }, + { + "options": [ + { + "question": { + "en": "Has a changing table" + }, + "osmTags": "changing_table=yes" + } + ] + }, + { + "options": [ + { + "question": { + "en": "Free to use" + }, + "osmTags": { + "or": [ + "fee=no", + "fee=0", + "charge=0" + ] + } + } + ] + } ] } \ No newline at end of file diff --git a/assets/themes/bookcases/bookcases.json b/assets/themes/bookcases/bookcases.json index 625dfa0ea..0862fbb37 100644 --- a/assets/themes/bookcases/bookcases.json +++ b/assets/themes/bookcases/bookcases.json @@ -39,7 +39,7 @@ "startLat": 0, "startLon": 0, "startZoom": 1, - "widenFactor": 0.05, + "widenFactor": 1, "roamingRenderings": [], "layers": [ "public_bookcase" diff --git a/assets/themes/drinking_water/drinking_water.json b/assets/themes/drinking_water/drinking_water.json index 9dee5a8a3..2134d0fba 100644 --- a/assets/themes/drinking_water/drinking_water.json +++ b/assets/themes/drinking_water/drinking_water.json @@ -34,7 +34,7 @@ "defaultBackgroundId": "CartoDB.Voyager", "startLon": 4.351697, "startZoom": 16, - "widenFactor": 0.05, + "widenFactor": 2, "layers": [ "drinking_water" ], diff --git a/assets/themes/natuurpunt/natuurpunt.json b/assets/themes/natuurpunt/natuurpunt.json index 4b45e9899..4616fae02 100644 --- a/assets/themes/natuurpunt/natuurpunt.json +++ b/assets/themes/natuurpunt/natuurpunt.json @@ -55,6 +55,7 @@ { "#": "Nature reserve overview from cache, points only, z < 13", "builtin": "nature_reserve", + "wayHandling": 1, "override": { "source": { "osmTags": { @@ -63,6 +64,7 @@ ] }, "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson", + "geoJsonZoomLevel": 0, "isOsmCache": "duplicate" }, "minzoom": 1, diff --git a/assets/themes/speelplekken/speelplekken.json b/assets/themes/speelplekken/speelplekken.json index 14e03b4d9..21cb17212 100644 --- a/assets/themes/speelplekken/speelplekken.json +++ b/assets/themes/speelplekken/speelplekken.json @@ -152,7 +152,7 @@ ] }, "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", - "geoJsonZoomLevel": 14, + "geoJsonZoomLevel": 11, "isOsmCache": true }, "title": { diff --git a/assets/themes/uk_addresses/uk_addresses.json b/assets/themes/uk_addresses/uk_addresses.json index 552f9c1eb..654bb2177 100644 --- a/assets/themes/uk_addresses/uk_addresses.json +++ b/assets/themes/uk_addresses/uk_addresses.json @@ -131,7 +131,7 @@ ] }, "then": { - "en": "This object has no house number" + "en": "This building has no house number" } } ] diff --git a/index.ts b/index.ts index 06f2ea851..0614e40a2 100644 --- a/index.ts +++ b/index.ts @@ -8,31 +8,16 @@ import MoreScreen from "./UI/BigComponents/MoreScreen"; import State from "./State"; import Combine from "./UI/Base/Combine"; import Translations from "./UI/i18n/Translations"; - - -import CountryCoder from "latlon2country" - -import SimpleMetaTagger from "./Logic/SimpleMetaTagger"; -import Minimap from "./UI/Base/Minimap"; -import DirectionInput from "./UI/Input/DirectionInput"; -import SpecialVisualizations from "./UI/SpecialVisualizations"; -import ShowDataLayer from "./UI/ShowDataLayer"; -import * as L from "leaflet"; import ValidatedTextField from "./UI/Input/ValidatedTextField"; import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; import Constants from "./Models/Constants"; +import MinimapImplementation from "./UI/Base/MinimapImplementation"; +MinimapImplementation.initialize() // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts -SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); -DirectionInput.constructMinimap = options => new Minimap(options) ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref) -SpecialVisualizations.constructMiniMap = options => new Minimap(options) -SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>, - leafletMap: UIEventSource, - layoutToUse: UIEventSource, - enablePopups = true, - zoomToFeatures = false) => new ShowDataLayer(features, leafletMap, layoutToUse, enablePopups, zoomToFeatures) + let defaultLayout = "" // --------------------- Special actions based on the parameters ----------------- diff --git a/langs/en.json b/langs/en.json index 09709dc9f..fdfc46fe5 100644 --- a/langs/en.json +++ b/langs/en.json @@ -159,7 +159,7 @@ "noTagsSelected": "No tags selected", "testing": "Testing - changes won't be saved", "customThemeIntro": "

Custom themes

These are previously visited user-generated themes.", - "aboutMapcomplete": "

About MapComplete

With MapComplete you can enrich OpenStreetMap with information on a single theme. Answer a few questions, and within minutes your contributions will be available around the globe! The theme maintainer defines elements, questions and languages for the theme.

Find out more

MapComplete always offers the next step to learn more about OpenStreetMap.

  • When embedded in a website, the iframe links to a full-screen MapComplete
  • The full-screen version offers information about OpenStreetMap
  • Viewing works without login, but editing requires an OSM login.
  • If you are not logged in, you are asked to log in
  • Once you answered a single question, you can add new points to the map
  • After a while, actual OSM-tags are shown, later linking to the wiki


Did you notice an issue? Do you have a feature request? Want to help translate? Head over to the source code or issue tracker.

Want to see your progress? Follow the edit count on OsmCha.

", + "aboutMapcomplete": "

About MapComplete

With MapComplete you can enrich OpenStreetMap with information on a single theme. Answer a few questions, and within minutes your contributions will be available around the globe! The theme maintainer defines elements, questions and languages for the theme.

Find out more

MapComplete always offers the next step to learn more about OpenStreetMap.

  • When embedded in a website, the iframe links to a full-screen MapComplete
  • The full-screen version offers information about OpenStreetMap
  • Viewing works without login, but editing requires an OSM login.
  • If you are not logged in, you are asked to log in
  • Once you answered a single question, you can add new points to the map
  • After a while, actual OSM-tags are shown, later linking to the wiki


Did you notice an issue? Do you have a feature request? Want to help translate? Head over to the source code or issue tracker.

Want to see your progress? Follow the edit count on OsmCha.

", "backgroundMap": "Background map", "openTheMap": "Open the map", "loginOnlyNeededToEdit": "if you want to edit the map", diff --git a/langs/layers/en.json b/langs/layers/en.json index d7910039a..9149f1966 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -3021,6 +3021,29 @@ } }, "toilet": { + "filter": { + "0": { + "options": { + "0": { + "question": "Wheelchair accessible" + } + } + }, + "1": { + "options": { + "0": { + "question": "Has a changing table" + } + } + }, + "2": { + "options": { + "0": { + "question": "Free to use" + } + } + } + }, "name": "Toilets", "presets": { "0": { diff --git a/langs/nl.json b/langs/nl.json index d774fea79..3b24c50cd 100644 --- a/langs/nl.json +++ b/langs/nl.json @@ -192,7 +192,7 @@ "getStartedNewAccount": " of maak een nieuwe account aan", "noTagsSelected": "Geen tags geselecteerd", "customThemeIntro": "

Onofficiële thema's

De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.", - "aboutMapcomplete": "

Over MapComplete

Met MapComplete kun je OpenStreetMap verrijken met informatie over een bepaald thema. Beantwoord enkele vragen, en binnen een paar minuten is jouw bijdrage wereldwijd beschikbaar! De maker van het thema bepaalt de elementen, vragen en taalversies voor het thema.

Ontdek meer

MapComplete biedt altijd de volgende stap naar meer OpenStreetMap:

  • Indien ingebed in een website linkt het iframe naar de volledige MapComplete
  • De volledige versie heeft uitleg over OpenStreetMap
  • Bekijken kan altijd, maar wijzigen vereist een OSM-account
  • Als je niet aangemeld bent, wordt je gevraagd dit te doen
  • Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen
  • Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki

Merk je een bug of wil je een extra feature? Wil je helpen vertalen? Bezoek dan de broncode en issue tracker.

Wil je je vorderingen zien? Volg de edits op OsmCha.

", + "aboutMapcomplete": "

Over MapComplete

Met MapComplete kun je OpenStreetMap verrijken met informatie over een bepaald thema. Beantwoord enkele vragen, en binnen een paar minuten is jouw bijdrage wereldwijd beschikbaar! De maker van het thema bepaalt de elementen, vragen en taalversies voor het thema.

Ontdek meer

MapComplete biedt altijd de volgende stap naar meer OpenStreetMap:

  • Indien ingebed in een website linkt het iframe naar de volledige MapComplete
  • De volledige versie heeft uitleg over OpenStreetMap
  • Bekijken kan altijd, maar wijzigen vereist een OSM-account
  • Als je niet aangemeld bent, wordt je gevraagd dit te doen
  • Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen
  • Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki

Merk je een bug of wil je een extra feature? Wil je helpen vertalen? Bezoek dan de broncode en issue tracker.

Wil je je vorderingen zien? Volg de edits op OsmCha.

", "backgroundMap": "Achtergrondkaart", "layerSelection": { "zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien", diff --git a/langs/themes/en.json b/langs/themes/en.json index 94eaf954b..e96960d80 100644 --- a/langs/themes/en.json +++ b/langs/themes/en.json @@ -1351,6 +1351,11 @@ "title": { "render": "Known address" } + }, + "2": { + "title": { + "render": "{name}" + } } }, "shortDescription": "Help to build an open dataset of UK addresses", diff --git a/package.json b/package.json index 8baeb62fd..31f2aaa7d 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "reset:translations": "ts-node scripts/generateTranslations.ts --ignore-weblate", "generate:layouts": "ts-node scripts/generateLayouts.ts", "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts", - "generate:cache:speelplekken:mini": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.181710380278176 4.423413276672363 51.193007664772495 4.444141387939452", + "generate:cache:speelplekken:mini": "ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache_mini/ 51.181710380278176 4.423413276672363 51.193007664772495 4.444141387939452", "generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56", "generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre", - "generate:layeroverview": "npm run generate:licenses && echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail", + "generate:layeroverview": "echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail", "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail", "query:licenses": "ts-node scripts/generateLicenseInfo.ts --query", "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push", diff --git a/scripts/ScriptUtils.ts b/scripts/ScriptUtils.ts index 702c40f4d..d150215e7 100644 --- a/scripts/ScriptUtils.ts +++ b/scripts/ScriptUtils.ts @@ -7,7 +7,6 @@ import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson"; Utils.runningFromConsole = true - export default class ScriptUtils { diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index c043bc224..c894d5442 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -2,27 +2,32 @@ * Generates a collection of geojson files based on an overpass query for a given theme */ import {Utils} from "../Utils"; + Utils.runningFromConsole = true + import {Overpass} from "../Logic/Osm/Overpass"; -import * as fs from "fs"; import {existsSync, readFileSync, writeFileSync} from "fs"; import {TagsFilter} from "../Logic/Tags/TagsFilter"; import {Or} from "../Logic/Tags/Or"; import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; -import ExtractRelations from "../Logic/Osm/ExtractRelations"; +import RelationsTracker from "../Logic/Osm/RelationsTracker"; import * as OsmToGeoJson from "osmtogeojson"; import MetaTagging from "../Logic/MetaTagging"; -import {GeoOperations} from "../Logic/GeoOperations"; import {UIEventSource} from "../Logic/UIEventSource"; import {TileRange} from "../Models/TileRange"; import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; -import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import ScriptUtils from "./ScriptUtils"; +import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter"; +import FilteredLayer from "../Models/FilteredLayer"; +import FeatureSource, {FeatureSourceForLayer} from "../Logic/FeatureSource/FeatureSource"; +import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; +import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource"; + ScriptUtils.fixUtils() -function createOverpassObject(theme: LayoutConfig) { +function createOverpassObject(theme: LayoutConfig, relationTracker: RelationsTracker) { let filters: TagsFilter[] = []; let extraScripts: string[] = []; for (const layer of theme.layers) { @@ -54,7 +59,7 @@ function createOverpassObject(theme: LayoutConfig) { throw "Nothing to download! The theme doesn't declare anything to download" } return new Overpass(new Or(filters), extraScripts, new UIEventSource("https://overpass.kumi.systems/api/interpreter"), //https://overpass-api.de/api/interpreter"), - new UIEventSource(60)); + new UIEventSource(60), relationTracker); } function rawJsonName(targetDir: string, x: number, y: number, z: number): string { @@ -75,7 +80,7 @@ async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/ downloaded++; const filename = rawJsonName(targetdir, x, y, r.zoomlevel) if (existsSync(filename)) { - console.log("Already exists: ", filename) + console.log("Already exists (not downloading again): ", filename) skipped++ continue; } @@ -145,14 +150,16 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ { return allFeatures; } -function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) { + +function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]): FeatureSource { + + let allFeatures = [...extraFeatures] let processed = 0; - const layerIndex = theme.LayerIndex(); for (let x = r.xstart; x <= r.xend; x++) { for (let y = r.ystart; y <= r.yend; y++) { processed++; const filename = rawJsonName(targetdir, x, y, r.zoomlevel) - ScriptUtils.erasableLog(" Post processing", processed, "/", r.total, filename) + console.log(" Loading and processing", processed, "/", r.total, filename) if (!existsSync(filename)) { console.error("Not found - and not downloaded. Run this script again!: " + filename) continue; @@ -163,152 +170,97 @@ function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extra // Create and save the geojson file - which is the main chunk of the data const geojson = OsmToGeoJson.default(rawOsm); - const osmTime = new Date(rawOsm.osm3s.timestamp_osm_base); - // And merge in the extra features - needed for the metatagging - geojson.features.push(...extraFeatures); - - for (const feature of geojson.features) { - - for (const layer of theme.layers) { - if (layer.source.osmTags.matchesProperties(feature.properties)) { - feature["_matching_layer_id"] = layer.id; - break; - } - } - } - const featuresFreshness = geojson.features.map(feature => { - return ({ - freshness: osmTime, - feature: feature - }); - }); - // Extract the relationship information - const relations = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(rawOsm)) - - MetaTagging.addMetatags(featuresFreshness, new UIEventSource<{ feature: any; freshness: Date }[]>(featuresFreshness), relations, theme.layers, false); - - - for (const feature of geojson.features) { - const layer = layerIndex.get(feature["_matching_layer_id"]) - if (layer === undefined) { - // Probably some extra, unneeded data, e.g. a point of a way - continue - } - - if (layer.wayHandling == LayerConfig.WAYHANDLING_CENTER_ONLY) { - - const centerpoint = GeoOperations.centerpointCoordinates(feature) - - feature.geometry.type = "Point" - feature.geometry["coordinates"] = centerpoint; - - } - } - for (const feature of geojson.features) { - // Some cleanup - delete feature["bbox"] - } - - const targetPath = geoJsonName(targetdir + ".unfiltered", x, y, r.zoomlevel) - // This is the geojson file containing all features - writeFileSync(targetPath, JSON.stringify(geojson, null, " ")) + allFeatures.push(...geojson.features) } } + return new StaticFeatureSource(allFeatures) } -function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) { - const z = r.zoomlevel; - const generated = {} // layer --> x --> y[] - for (let x = r.xstart; x <= r.xend; x++) { - for (let y = r.ystart; y <= r.yend; y++) { - const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8") +/** + * Load all the tiles into memory from disk + */ +function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string) { - for (const layer of theme.layers) { - if (!layer.source.isOsmCacheLayer) { - continue; - } - const geojson = JSON.parse(file) - const oldLength = geojson.features.length; - geojson.features = geojson.features - .filter(f => f._matching_layer_id === layer.id) - .filter(f => { - const isShown = layer.isShown.GetRenderValue(f.properties).txt - return isShown !== "no"; - }) - const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z); - ScriptUtils.erasableLog(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")") - if (geojson.features.length == 0) { - continue; + function handleLayer(source: FeatureSourceForLayer) { + const layer = source.layer.layerDef; + const layerId = layer.id + if (layer.source.isOsmCacheLayer !== true) { + return; + } + console.log("Handling layer ", layerId, "which has", source.features.data.length, "features") + if (source.features.data.length === 0) { + return; + } + MetaTagging.addMetatags(source.features.data, + { + memberships: relationsTracker, + getFeaturesWithin: _ => { + return [allFeatures.features.data.map(f => f.feature)] } - writeFileSync(new_path, JSON.stringify(geojson, null, " ")) + }, + layer, + false); - if (generated[layer.id] === undefined) { - generated[layer.id] = {} + const createdTiles = [] + // At this point, we have all the features of the entire area. + // However, we want to export them per tile of a fixed size, so we use a dynamicTileSOurce to split it up + TiledFeatureSource.createHierarchy(source, { + minZoomLevel: 14, + maxZoomLevel: 14, + maxFeatureCount: undefined, + registerTile: tile => { + if (tile.z < 12) { + return; } - if (generated[layer.id][x] === undefined) { - generated[layer.id][x] = [] + if (tile.features.data.length === 0) { + return } - generated[layer.id][x].push(y) - + for (const feature of tile.features.data) { + // Some cleanup + delete feature.feature["bbox"] + } + // Lets save this tile! + const [z, x, y] = Utils.tile_from_index(tile.tileIndex) + console.log("Writing tile ", z, x, y, layerId) + const targetPath = geoJsonName(targetdir + "_" + layerId, x, y, z) + createdTiles.push(tile.tileIndex) + // This is the geojson file containing all features for this tile + writeFileSync(targetPath, JSON.stringify({ + type: "FeatureCollection", + features: tile.features.data.map(f => f.feature) + }, null, " ")) } - } - } - - for (const layer of theme.layers) { - const id = layer.id - const loaded = generated[id] - if (loaded === undefined) { - console.log("No features loaded for layer ", id) - continue; - } - writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded)) + }) + + // All the tiles are written at this point + // Only thing left to do is to create the index + const path = targetdir + "_" + layerId + "_overview.json" + const perX = {} + createdTiles.map(i => Utils.tile_from_index(i)).forEach(([z, x, y]) => { + const key = "" + x + if (perX[key] === undefined) { + perX[key] = [] + } + perX[key].push(y) + }) + writeFileSync(path, JSON.stringify(perX)) + + } + new PerLayerFeatureSourceSplitter( + new UIEventSource(theme.layers.map(l => ({ + layerDef: l, + isDisplayed: new UIEventSource(true), + appliedFilters: new UIEventSource(undefined) + }))), + handleLayer, + allFeatures + ) } -async function createOverview(targetdir: string, r: TileRange, z: number, layername: string) { - const allFeatures = [] - for (let x = r.xstart; x <= r.xend; x++) { - for (let y = r.ystart; y <= r.yend; y++) { - const read_path = geoJsonName(targetdir + "_" + layername, x, y, z); - if (!fs.existsSync(read_path)) { - continue; - } - const features = JSON.parse(fs.readFileSync(read_path, "UTF-8")).features - const pointsOnly = features.map(f => { - - f.properties["_last_edit:timestamp"] = "1970-01-01" - - if (f.geometry.type === "Point") { - return f - } else { - return GeoOperations.centerpoint(f) - } - - }) - allFeatures.push(...pointsOnly) - } - } - - const featuresDedup = [] - const seen = new Set() - for (const feature of allFeatures) { - const id = feature.properties.id - if (seen.has(id)) { - continue - } - seen.add(id) - featuresDedup.push(feature) - } - - const geojson = { - "type": "FeatureCollection", - "features": featuresDedup - } - writeFileSync(targetdir + "_" + layername + "_points.geojson", JSON.stringify(geojson, null, " ")) -} async function main(args: string[]) { @@ -335,8 +287,8 @@ async function main(args: string[]) { console.error("The theme " + theme + " was not found; try one of ", keys); return } - - const overpass = createOverpassObject(theme) + const relationTracker = new RelationsTracker() + const overpass = createOverpassObject(theme, relationTracker) let failed = 0; do { @@ -348,21 +300,13 @@ async function main(args: string[]) { } while (failed > 0) const extraFeatures = await downloadExtraData(theme); - postProcess(targetdir, tileRange, theme, extraFeatures) - splitPerLayer(targetdir, tileRange, theme) + const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures) + postProcess(allFeaturesSource, theme, relationTracker, targetdir) - if (args[7] === "--generate-point-overview") { - const targetLayers = args[8].split(",") - for (const targetLayer of targetLayers) { - if (!theme.layers.some(l => l.id === targetLayer)) { - throw "Target layer " + targetLayer + " not found, did you mistype the name? Found layers are: " + theme.layers.map(l => l.id).join(",") - } - createOverview(targetdir, tileRange, zoomlevel, targetLayer) - } - } } let args = [...process.argv] args.splice(0, 2) -main(args); \ No newline at end of file +main(args); +console.log("All done!") \ No newline at end of file