More refactoring, move minimap behind facade

This commit is contained in:
Pieter Vander Vennet 2021-09-21 02:10:42 +02:00
parent c11ff652b8
commit d5c1ba4cd1
79 changed files with 1848 additions and 1118 deletions

View file

@ -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<BaseLayer>,
location: UIEventSource<Loc>,

View file

@ -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<boolean> = new UIEventSource<boolean>(false);
public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0);
public readonly relationsTracker: RelationsTracker;
private readonly retries: UIEventSource<number> = new UIEventSource<number>(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<number, Bounds[]> = new Map<number, Bounds[]>();
private readonly _location: UIEventSource<Loc>;
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
private readonly _leafletMap: UIEventSource<L.Map>;
private readonly _interpreterUrl: UIEventSource<string>;
private readonly _timeout: UIEventSource<number>;
private readonly state: {
readonly locationControl: UIEventSource<Loc>,
readonly layoutToUse: UIEventSource<LayoutConfig>,
readonly leafletMap: any,
readonly overpassUrl: UIEventSource<string>;
readonly overpassTimeout: UIEventSource<number>;
}
/**
* The most important layer should go first, as that one gets first pick for the questions
*/
constructor(
location: UIEventSource<Loc>,
layoutToUse: UIEventSource<LayoutConfig>,
leafletMap: UIEventSource<L.Map>,
interpreterUrl: UIEventSource<string>,
timeout: UIEventSource<number>,
maxZoom = undefined) {
this._location = location;
this._layoutToUse = layoutToUse;
this._leafletMap = leafletMap;
this._interpreterUrl = interpreterUrl;
this._timeout = timeout;
state: {
readonly locationControl: UIEventSource<Loc>,
readonly layoutToUse: UIEventSource<LayoutConfig>,
readonly leafletMap: any,
readonly overpassUrl: UIEventSource<string>;
readonly overpassTimeout: UIEventSource<number>;
readonly overpassMaxZoom: UIEventSource<number>
}) {
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 &&

View file

@ -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.

View file

@ -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<Map<string, number>>;
public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>());
private readonly state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> };
constructor(featureSource: FeatureSource) {
this.Contributors = featureSource.features.map(features => {
const hist = new Map<string, number>();
for (const feature of features) {
const contributor = feature.feature.properties["_last_edit:contributor"]
constructor(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> }) {
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<string, number>();
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)
}
}

View file

@ -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<string, any[]>, 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<string, any[]>, 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<string, any[]>, 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<string, any[]>, 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)
}
}

View file

@ -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)

View file

@ -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 }[]>;

View file

@ -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<ChangeDescription>();
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<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>()
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 => {

View file

@ -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<boolean>;
public readonly runningQuery: UIEventSource<boolean>;
public readonly timeout: UIEventSource<number>;
public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false)
constructor(flayers: UIEventSource<FilteredLayer[]>,
changes: Changes,
updater: FeatureSource,
fromOsmApi: FeatureSource,
layout: UIEventSource<LayoutConfig>,
locationControl: UIEventSource<Loc>,
selectedElement: UIEventSource<any>) {
private readonly overpassUpdater: OverpassFeatureSource
private readonly relationTracker: RelationsTracker
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>;
constructor(
handleFeatureSource: (source: FeatureSourceForLayer) => void,
state: {
osmApiFeatureSource: FeatureSource,
filteredLayers: UIEventSource<FilteredLayer[]>,
locationControl: UIEventSource<Loc>,
selectedElement: UIEventSource<any>,
changes: Changes,
layoutToUse: UIEventSource<LayoutConfig>,
leafletMap: any,
readonly overpassUrl: UIEventSource<string>;
readonly overpassTimeout: UIEventSource<number>;
readonly overpassMaxZoom: UIEventSource<number>;
}) {
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<string, TileHierarchyMerger>()
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()
}
}

View file

@ -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<Set<string>>
}
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}
}
}
/**
* A feature source which has some extra data about it's state
*/
export interface FeatureSourceState {
readonly sufficientlyZoomed: UIEventSource<boolean>;
readonly runningQuery: UIEventSource<boolean>;
readonly timeout: UIEventSource<number>;
}

View file

@ -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<FilteredLayer[]>,
handleLayerData: (source: FeatureSource) => void,
upstream: OverpassFeatureSource) {
handleLayerData: (source: FeatureSourceForLayer) => void,
upstream: FeatureSource) {
const knownLayers = new Map<string, FeatureSource>()
const knownLayers = new Map<string, FeatureSourceForLayer>()
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())
}
}

View file

@ -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<FeatureSource[]>;
public readonly tileIndex: number;
public readonly bbox: BBox;
constructor(layer: FilteredLayer ,sources: UIEventSource<FeatureSource[]>) {
constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
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<FeatureSource>();

View file

@ -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;

View file

@ -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<string> = new Set<string>()
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;

View file

@ -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 {

View file

@ -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 }[]>;

View file

@ -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 }[]>([]);

View file

@ -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<any[]>, 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
})))
}
}

View file

@ -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;

View file

@ -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<Loc>
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<number, Set<number>>();
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
);
}
}

View file

@ -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<FeatureSourceForLayer & Tiled> {
private readonly _loadedTiles = new Set<number>();
public readonly existingTiles: Map<number, Map<number, FeatureSourceForLayer>> = new Map<number, Map<number, FeatureSourceForLayer>>()
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>;
constructor(
layer: FilteredLayer,
zoomlevel: number,
constructTile: (xy: [number, number]) => FeatureSourceForLayer,
constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled),
state: {
locationControl: UIEventSource<Loc>
leafletMap: any
@ -24,6 +26,8 @@ export default class DynamicTileSource {
) {
state = State.state
const self = this;
this.loadedTiles = new Map<number,FeatureSourceForLayer & Tiled>()
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<number, FeatureSourceForLayer>()
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)
}
})
}
}
}

View file

@ -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
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

View file

@ -0,0 +1,25 @@
import FeatureSource, {Tiled} from "../FeatureSource";
import {BBox} from "../../GeoOperations";
export default interface TileHierarchy<T extends FeatureSource & Tiled> {
/**
* A mapping from 'tile_index' to the actual tile featrues
*/
loadedTiles: Map<number, T>
}
export class TileHierarchyTools {
public static getTiles<T extends FeatureSource & Tiled>(hierarchy: TileHierarchy<T>, bbox: BBox): T[] {
const result = []
hierarchy.loadedTiles.forEach((tile) => {
if (tile.bbox.overlapsWith(bbox)) {
result.push(tile)
}
})
return result;
}
}

View file

@ -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<FeatureSourceForLayer & Tiled> {
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
@ -24,8 +24,9 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer
* @param src
* @param index
*/
public registerTile(src: FeatureSource, index: number) {
public registerTile(src: FeatureSource & Tiled) {
const index = src.tileIndex
if (this.sources.has(index)) {
const sources = this.sources.get(index)
sources.data.push(src)

View file

@ -0,0 +1,191 @@
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
import {Utils} from "../../../Utils";
import {BBox} from "../../GeoOperations";
import FilteredLayer from "../../../Models/FilteredLayer";
import TileHierarchy from "./TileHierarchy";
import {feature} from "@turf/turf";
/**
* Contains all features in a tiled fashion.
* The data will be automatically broken down into subtiles when there are too much features in a single tile or if the zoomlevel is too high
*/
export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, FeatureSourceForLayer, TileHierarchy<IndexedFeatureSource & FeatureSourceForLayer & Tiled> {
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<number, TiledFeatureSource & FeatureSourceForLayer> = undefined;
public readonly maxFeatureCount: number;
public readonly name;
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>
public readonly containedIds: UIEventSource<Set<string>>
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<any[]>([])
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
}

View file

@ -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<FeatureSourceForLayer & Tiled> {
public loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
export default class DynamicGeoJsonTileSource extends DynamicTileSource {
constructor(layer: FilteredLayer,
registerLayer: (layer: FeatureSourceForLayer) => void,
handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void,
state: {
locationControl: UIEventSource<Loc>
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)
}
}
})
}
}

View file

@ -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
}
}

View file

@ -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))};

View file

@ -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<string, any[]>,
memberships: Map<string, { role: string, relation: Relation }[]>
}
/**
* 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<string, { role: string, relation: Relation }[]>,
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<string, ((params: Params, feature: any) => void)>();
for (const layer of layers) {
layerFuncs.set(layer.id, this.createRetaggingFunc(layer));
}
allKnownFeatures.addCallbackAndRunD(newFeatures => {
const featuresPerLayer = new Map<string, any[]>();
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);

View file

@ -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

View file

@ -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;

View file

@ -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<number>;
private readonly _extraScripts: string[];
private _includeMeta: boolean;
private _relationTracker: RelationsTracker;
constructor(filter: TagsFilter, extraScripts: string[],
interpreterUrl: UIEventSource<string>,
timeout: UIEventSource<number>,
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 {

View file

@ -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<Map<string, { role: string; relation: Relation }[]>>(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<string, { role: string, relation: Relation }[]> {
const memberships = new Map<string, { role: string, relation: Relation }[]>()
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
}
}

View file

@ -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);

View file

@ -81,9 +81,12 @@ export class UIEventSource<T> {
return this;
}
public addCallbackAndRun(callback: ((latestData: T) => void)): UIEventSource<T> {
callback(this.data);
return this.addCallback(callback);
public addCallbackAndRun(callback: ((latestData: T) => (boolean | void | any))): UIEventSource<T> {
const doDeleteCallback = callback(this.data);
if (!doDeleteCallback) {
this.addCallback(callback);
}
return this;
}
public setData(t: T): UIEventSource<T> {