More refactoring, move minimap behind facade

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

View file

@ -1,7 +1,6 @@
import {FixedUiElement} from "./UI/Base/FixedUiElement"; import {FixedUiElement} from "./UI/Base/FixedUiElement";
import Toggle from "./UI/Input/Toggle"; import Toggle from "./UI/Input/Toggle";
import State from "./State"; import State from "./State";
import LoadFromOverpass from "./Logic/Actors/OverpassFeatureSource";
import {UIEventSource} from "./Logic/UIEventSource"; import {UIEventSource} from "./Logic/UIEventSource";
import {QueryParameters} from "./Logic/Web/QueryParameters"; import {QueryParameters} from "./Logic/Web/QueryParameters";
import StrayClickHandler from "./Logic/Actors/StrayClickHandler"; import StrayClickHandler from "./Logic/Actors/StrayClickHandler";
@ -18,17 +17,15 @@ import * as L from "leaflet";
import Img from "./UI/Base/Img"; import Img from "./UI/Base/Img";
import UserDetails from "./Logic/Osm/OsmConnection"; import UserDetails from "./Logic/Osm/OsmConnection";
import Attribution from "./UI/BigComponents/Attribution"; 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 FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs";
import ShowDataLayer from "./UI/ShowDataLayer"; import ShowDataLayer from "./UI/ShowDataLayer/ShowDataLayer";
import Hash from "./Logic/Web/Hash"; import Hash from "./Logic/Web/Hash";
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
import ScrollableFullScreen from "./UI/Base/ScrollableFullScreen"; import ScrollableFullScreen from "./UI/Base/ScrollableFullScreen";
import Translations from "./UI/i18n/Translations"; import Translations from "./UI/i18n/Translations";
import MapControlButton from "./UI/MapControlButton"; import MapControlButton from "./UI/MapControlButton";
import SelectedFeatureHandler from "./Logic/Actors/SelectedFeatureHandler";
import LZString from "lz-string"; import LZString from "lz-string";
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
import AllKnownLayers from "./Customizations/AllKnownLayers"; import AllKnownLayers from "./Customizations/AllKnownLayers";
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
import {TagsFilter} from "./Logic/Tags/TagsFilter"; import {TagsFilter} from "./Logic/Tags/TagsFilter";
@ -38,7 +35,6 @@ import {LayoutConfigJson} from "./Models/ThemeConfig/Json/LayoutConfigJson";
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
import LayerConfig from "./Models/ThemeConfig/LayerConfig"; import LayerConfig from "./Models/ThemeConfig/LayerConfig";
import Minimap from "./UI/Base/Minimap"; import Minimap from "./UI/Base/Minimap";
import Constants from "./Models/Constants";
export class InitUiElements { export class InitUiElements {
static InitAll( static InitAll(
@ -130,10 +126,9 @@ export class InitUiElements {
} }
} }
if (somethingChanged) { if (somethingChanged) {
console.log("layoutToUse.layers:", layoutToUse.layers);
State.state.layoutToUse.data.layers = Array.from(neededLayers); State.state.layoutToUse.data.layers = Array.from(neededLayers);
State.state.layoutToUse.ping(); State.state.layoutToUse.ping();
State.state.layerUpdater?.ForceRefresh(); State.state.featurePipeline?.ForceRefresh();
} }
} }
@ -320,7 +315,7 @@ export class InitUiElements {
(layer) => layer.id (layer) => layer.id
); );
new LayerResetter( new BackgroundLayerResetter(
State.state.backgroundLayer, State.state.backgroundLayer,
State.state.locationControl, State.state.locationControl,
State.state.availableBackgroundLayers, State.state.availableBackgroundLayers,
@ -333,13 +328,14 @@ export class InitUiElements {
State.state.locationControl, State.state.locationControl,
State.state.osmConnection.userDetails, State.state.osmConnection.userDetails,
State.state.layoutToUse, State.state.layoutToUse,
State.state.leafletMap State.state.currentBounds
); );
new Minimap({ Minimap.createMiniMap({
background: State.state.backgroundLayer, background: State.state.backgroundLayer,
location: State.state.locationControl, location: State.state.locationControl,
leafletMap: State.state.leafletMap, leafletMap: State.state.leafletMap,
bounds: State.state.currentBounds,
attribution: attr, attribution: attr,
lastClickLocation: State.state.LastClickLocation lastClickLocation: State.state.LastClickLocation
}).SetClass("w-full h-full") }).SetClass("w-full h-full")
@ -371,7 +367,7 @@ export class InitUiElements {
} }
} }
private static InitLayers(): FeatureSource { private static InitLayers(): void {
const state = State.state; const state = State.state;
state.filteredLayers = state.layoutToUse.map((layoutToUse) => { state.filteredLayers = state.layoutToUse.map((layoutToUse) => {
const flayers = []; const flayers = [];
@ -396,51 +392,35 @@ export class InitUiElements {
return flayers; return flayers;
}); });
const updater = new LoadFromOverpass( State.state.featurePipeline = new FeaturePipeline(
state.locationControl, source => {
state.layoutToUse, new ShowDataLayer(
state.leafletMap, {
state.overpassUrl, features: source,
state.overpassTimeout, leafletMap: State.state.leafletMap,
Constants.useOsmApiAt layerToShow: source.layer.layerDef
); }
State.state.layerUpdater = updater; );
}, state
const source = new FeaturePipeline(
state.filteredLayers,
State.state.changes,
updater,
state.osmApiFeatureSource,
state.layoutToUse,
state.locationControl,
state.selectedElement
); );
State.state.featurePipeline = source; /* const selectedFeatureHandler = new SelectedFeatureHandler(
new ShowDataLayer( Hash.hash,
source.features, State.state.selectedElement,
State.state.leafletMap, source,
State.state.layoutToUse State.state.osmApiFeatureSource
); );
selectedFeatureHandler.zoomToSelectedFeature(
const selectedFeatureHandler = new SelectedFeatureHandler( State.state.locationControl
Hash.hash, );*/
State.state.selectedElement,
source,
State.state.osmApiFeatureSource
);
selectedFeatureHandler.zoomToSelectedFeature(
State.state.locationControl
);
return source;
} }
private static setupAllLayerElements() { private static setupAllLayerElements() {
// ------------- Setup the layers ------------------------------- // ------------- 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"); new RightControls().AttachTo("bottom-right");
// ------------------ Setup various other UI elements ------------ // ------------------ Setup various other UI elements ------------

View file

@ -6,7 +6,7 @@ import Loc from "../../Models/Loc";
/** /**
* Sets the current background layer to a layer that is actually available * Sets the current background layer to a layer that is actually available
*/ */
export default class LayerResetter { export default class BackgroundLayerResetter {
constructor(currentBackgroundLayer: UIEventSource<BaseLayer>, constructor(currentBackgroundLayer: UIEventSource<BaseLayer>,
location: UIEventSource<Loc>, location: UIEventSource<Loc>,

View file

@ -3,14 +3,15 @@ import Loc from "../../Models/Loc";
import {Or} from "../Tags/Or"; import {Or} from "../Tags/Or";
import {Overpass} from "../Osm/Overpass"; import {Overpass} from "../Osm/Overpass";
import Bounds from "../../Models/Bounds"; import Bounds from "../../Models/Bounds";
import FeatureSource from "../FeatureSource/FeatureSource"; import FeatureSource, {FeatureSourceState} from "../FeatureSource/FeatureSource";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {TagsFilter} from "../Tags/TagsFilter"; import {TagsFilter} from "../Tags/TagsFilter";
import SimpleMetaTagger from "../SimpleMetaTagger"; import SimpleMetaTagger from "../SimpleMetaTagger";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; 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" 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 runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false);
public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0); public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0);
public readonly relationsTracker: RelationsTracker;
private readonly retries: UIEventSource<number> = new UIEventSource<number>(0); private readonly retries: UIEventSource<number> = new UIEventSource<number>(0);
/** /**
* The previous bounds for which the query has been run at the given zoom level * 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 * 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 _previousBounds: Map<number, Bounds[]> = new Map<number, Bounds[]>();
private readonly _location: UIEventSource<Loc>; private readonly state: {
private readonly _layoutToUse: UIEventSource<LayoutConfig>; readonly locationControl: UIEventSource<Loc>,
private readonly _leafletMap: UIEventSource<L.Map>; readonly layoutToUse: UIEventSource<LayoutConfig>,
private readonly _interpreterUrl: UIEventSource<string>; readonly leafletMap: any,
private readonly _timeout: UIEventSource<number>; readonly overpassUrl: UIEventSource<string>;
readonly overpassTimeout: UIEventSource<number>;
}
/** /**
* The most important layer should go first, as that one gets first pick for the questions * The most important layer should go first, as that one gets first pick for the questions
*/ */
constructor( constructor(
location: UIEventSource<Loc>, state: {
layoutToUse: UIEventSource<LayoutConfig>, readonly locationControl: UIEventSource<Loc>,
leafletMap: UIEventSource<L.Map>, readonly layoutToUse: UIEventSource<LayoutConfig>,
interpreterUrl: UIEventSource<string>, readonly leafletMap: any,
timeout: UIEventSource<number>, readonly overpassUrl: UIEventSource<string>;
maxZoom = undefined) { readonly overpassTimeout: UIEventSource<number>;
this._location = location; readonly overpassMaxZoom: UIEventSource<number>
this._layoutToUse = layoutToUse; }) {
this._leafletMap = leafletMap;
this._interpreterUrl = interpreterUrl;
this._timeout = timeout; this.state = state
this.relationsTracker = new RelationsTracker()
const location = state.locationControl
const self = this; const self = this;
this.sufficientlyZoomed = location.map(location => { this.sufficientlyZoomed = location.map(location => {
if (location?.zoom === undefined) { if (location?.zoom === undefined) {
return false; return false;
} }
let minzoom = Math.min(...layoutToUse.data.layers.map(layer => layer.minzoom ?? 18)); let minzoom = Math.min(...state.layoutToUse.data.layers.map(layer => layer.minzoom ?? 18));
if(location.zoom < minzoom){ if (location.zoom < minzoom) {
return false; return false;
} }
if(maxZoom !== undefined && location.zoom > maxZoom){ const maxZoom = state.overpassMaxZoom.data
if (maxZoom !== undefined && location.zoom > maxZoom) {
return false; return false;
} }
return true; return true;
}, [layoutToUse] }, [state.layoutToUse]
); );
for (let i = 0; i < 25; i++) { for (let i = 0; i < 25; i++) {
// This update removes all data on all layers -> erase the map on lower levels too // This update removes all data on all layers -> erase the map on lower levels too
this._previousBounds.set(i, []); this._previousBounds.set(i, []);
} }
layoutToUse.addCallback(() => { state.layoutToUse.addCallback(() => {
self.update() self.update()
}); });
location.addCallback(() => { location.addCallback(() => {
self.update() self.update()
}); });
leafletMap.addCallbackAndRunD(_ => { state.leafletMap.addCallbackAndRunD(_ => {
self.update(); self.update();
}) })
} }
@ -97,11 +106,11 @@ export default class OverpassFeatureSource implements FeatureSource {
private GetFilter(): Overpass { private GetFilter(): Overpass {
let filters: TagsFilter[] = []; let filters: TagsFilter[] = [];
let extraScripts: string[] = []; let extraScripts: string[] = [];
for (const layer of this._layoutToUse.data.layers) { for (const layer of this.state.layoutToUse.data.layers) {
if (typeof (layer) === "string") { if (typeof (layer) === "string") {
throw "A layer was not expanded!" throw "A layer was not expanded!"
} }
if (this._location.data.zoom < layer.minzoom) { if (this.state.locationControl.data.zoom < layer.minzoom) {
continue; continue;
} }
if (layer.doNotDownload) { if (layer.doNotDownload) {
@ -141,7 +150,7 @@ export default class OverpassFeatureSource implements FeatureSource {
if (filters.length + extraScripts.length === 0) { if (filters.length + extraScripts.length === 0) {
return undefined; 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 { private update(): void {
@ -155,21 +164,22 @@ export default class OverpassFeatureSource implements FeatureSource {
return; 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) { if (bounds === undefined) {
return; return;
} }
const n = Math.min(90, bounds.getNorth() ); const n = Math.min(90, bounds.getNorth());
const e = Math.min(180, bounds.getEast() ); const e = Math.min(180, bounds.getEast());
const s = Math.max(-90, bounds.getSouth()); const s = Math.max(-90, bounds.getSouth());
const w = Math.max(-180, bounds.getWest()); const w = Math.max(-180, bounds.getWest());
const queryBounds = {north: n, east: e, south: s, west: w}; 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 self = this;
const overpass = this.GetFilter(); const overpass = this.GetFilter();
if (overpass === undefined) { if (overpass === undefined) {
return; return;
} }
@ -181,14 +191,18 @@ export default class OverpassFeatureSource implements FeatureSource {
const features = data.features.map(f => ({feature: f, freshness: date})); const features = data.features.map(f => ({feature: f, freshness: date}));
SimpleMetaTagger.objectMetaInfo.addMetaTags(features) 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); self.runningQuery.setData(false);
}, },
function (reason) { function (reason) {
self.retries.data++; self.retries.data++;
self.ForceRefresh(); self.ForceRefresh();
self.timeout.setData(self.retries.data * 5); 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.retries.ping();
self.runningQuery.setData(false); self.runningQuery.setData(false);
@ -222,7 +236,7 @@ export default class OverpassFeatureSource implements FeatureSource {
return false; return false;
} }
const b = this._leafletMap.data.getBounds(); const b = this.state.leafletMap.data.getBounds();
return b.getSouth() >= bounds.south && return b.getSouth() >= bounds.south &&
b.getNorth() <= bounds.north && b.getNorth() <= bounds.north &&
b.getEast() <= bounds.east && b.getEast() <= bounds.east &&

View file

@ -3,7 +3,7 @@ import FeatureSource from "../FeatureSource/FeatureSource";
import {OsmObject} from "../Osm/OsmObject"; import {OsmObject} from "../Osm/OsmObject";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import FeaturePipeline from "../FeatureSource/FeaturePipeline"; 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. * 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 /// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
import FeatureSource from "./FeatureSource/FeatureSource"; import FeatureSource from "./FeatureSource/FeatureSource";
import {UIEventSource} from "./UIEventSource"; 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 { 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) { constructor(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> }) {
this.Contributors = featureSource.features.map(features => { this.state = state;
const hist = new Map<string, number>(); const self = this;
for (const feature of features) { state.currentBounds.map(bbox => {
const contributor = feature.feature.properties["_last_edit:contributor"] 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; const count = hist.get(contributor) ?? 0;
hist.set(contributor, count + 1) 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 Combine from "../UI/Base/Combine";
import {Relation} from "./Osm/ExtractRelations"; import RelationsTracker from "./Osm/RelationsTracker";
import State from "../State"; import State from "../State";
import {Utils} from "../Utils";
import BaseUIElement from "../UI/BaseUIElement"; import BaseUIElement from "../UI/BaseUIElement";
import List from "../UI/Base/List"; import List from "../UI/Base/List";
import Title from "../UI/Base/Title"; import Title from "../UI/Base/Title";
import {UIEventSourceTools} from "./UIEventSource"; import {UIEventSourceTools} from "./UIEventSource";
import AspectedRouting from "./Osm/aspectedRouting"; 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 { export class ExtraFunction {
@ -55,15 +65,20 @@ export class ExtraFunction {
(params, feat) => { (params, feat) => {
return (...layerIds: string[]) => { return (...layerIds: string[]) => {
const result = [] const result = []
const bbox = BBox.get(feat)
for (const layerId of layerIds) { for (const layerId of layerIds) {
const otherLayer = params.featuresPerLayer.get(layerId); const otherLayers = params.getFeaturesWithin(layerId, bbox)
if (otherLayer === undefined) { if (otherLayers === undefined) {
continue; continue;
} }
if (otherLayer.length === 0) { if (otherLayers.length === 0) {
continue; continue;
} }
result.push(...GeoOperations.calculateOverlap(feat, otherLayer)); for (const otherLayer of otherLayers) {
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
}
} }
return result; return result;
} }
@ -77,6 +92,9 @@ export class ExtraFunction {
}, },
(featuresPerLayer, feature) => { (featuresPerLayer, feature) => {
return (arg0, lat) => { return (arg0, lat) => {
if (arg0 === undefined) {
return undefined;
}
if (typeof arg0 === "number") { if (typeof arg0 === "number") {
// Feature._lon and ._lat is conveniently place by one of the other metatags // Feature._lon and ._lat is conveniently place by one of the other metatags
return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]); return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]);
@ -103,7 +121,7 @@ export class ExtraFunction {
args: ["list of features"] args: ["list of features"]
}, },
(params, feature) => { (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. " + 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" + "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)", "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) => { (params, feature) => {
return (features, amount, uniqueTag) => ExtraFunction.GetClosestNFeatures(params, feature, features, { return (features, amount, uniqueTag, maxDistanceInMeters) => ExtraFunction.GetClosestNFeatures(params, feature, features, {
maxFeatures: Number(amount), 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(';')`", "For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`",
args: [] args: []
}, },
(params, _) => { (params, feat) => {
return () => params.relations ?? []; return () =>
params.memberships.knownRelations.data.get(feat.properties.id) ?? []
} }
) )
private static readonly AspectedRouting = new ExtraFunction( private static readonly AspectedRouting = new ExtraFunction(
@ -165,19 +186,19 @@ export class ExtraFunction {
private readonly _name: string; private readonly _name: string;
private readonly _args: string[]; private readonly _args: string[];
private readonly _doc: 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[] }, 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._name = options.name;
this._doc = options.doc; this._doc = options.doc;
this._args = options.args; this._args = options.args;
this._f = f; 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) { 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 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") { if (typeof features === "string") {
const name = features const name = features
features = params.featuresPerLayer.get(features) const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance))
if (features === undefined) { features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates))
var keys = Utils.NoNull(Array.from(params.featuresPerLayer.keys())); }else{
if (keys.length > 0) { features = [features]
throw `No features defined for ${name}. Defined layers are ${keys.join(", ")}`; }
} else { if (features === undefined) {
// This is the first pass over an external dataset return;
// Other data probably still has to load!
return undefined;
}
}
} }
let closestFeatures: { feat: any, distance: number }[] = []; let closestFeatures: { feat: any, distance: number }[] = [];
for (const otherFeature of features) { for(const featureList of features) {
if (otherFeature == feature || otherFeature.id == feature.id) { for (const otherFeature of featureList) {
continue; // We ignore self if (otherFeature == feature || otherFeature.id == feature.id) {
} continue; // We ignore self
let distance = undefined; }
if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) { let distance = undefined;
distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]); if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) {
} else { distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]);
distance = GeoOperations.distanceBetween( } else {
GeoOperations.centerpointCoordinates(otherFeature), distance = GeoOperations.distanceBetween(
[feature._lon, feature._lat] GeoOperations.centerpointCoordinates(otherFeature),
) [feature._lon, feature._lat]
} )
if (distance === undefined) { }
throw "Undefined distance!" if (distance === undefined) {
} throw "Undefined distance!"
}
if (distance > maxDistance) {
continue
}
if (closestFeatures.length === 0) { if (closestFeatures.length === 0) {
closestFeatures.push({ closestFeatures.push({
feat: otherFeature, feat: otherFeature,
distance: distance distance: distance
}) })
continue; continue;
} }
if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) {
// The last feature of the list (and thus the furthest away is still closer // 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! // No use for checking, as we already have plenty of features!
continue continue
} }
let targetIndex = closestFeatures.length let targetIndex = closestFeatures.length
for (let i = 0; i < closestFeatures.length; i++) { for (let i = 0; i < closestFeatures.length; i++) {
const closestFeature = closestFeatures[i]; const closestFeature = closestFeatures[i];
if (uniqueTag !== undefined) { if (uniqueTag !== undefined) {
const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined && const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined &&
closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag] closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag]
if (uniqueTagsMatch) { if (uniqueTagsMatch) {
targetIndex = -1 targetIndex = -1
if (closestFeature.distance > distance) { if (closestFeature.distance > distance) {
// This is a very special situation: // 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') // 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 // AT this point, we have found a closer segment with the same, identical tag
// so we replace directly // so we replace directly
closestFeatures[i] = {feat: otherFeature, distance: distance} 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; break;
} }
} }
if (closestFeature.distance > distance) { if (targetIndex == -1) {
targetIndex = i continue; // value is already swapped by the unique tag
}
if (uniqueTag !== undefined) { if (targetIndex < maxFeatures) {
const uniqueValue = otherFeature.properties[uniqueTag] // insert and drop one
// We might still have some other values later one with the same uniquetag that have to be cleaned closestFeatures.splice(targetIndex, 0, {
for (let j = i; j < closestFeatures.length; j++) { feat: otherFeature,
if(closestFeatures[j].feat.properties[uniqueTag] === uniqueValue){ distance: distance
closestFeatures.splice(j, 1) })
} 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; return closestFeatures;
} }
public PatchFeature(featuresPerLayer: Map<string, any[]>, relations: { role: string, relation: Relation }[], feature: any) { public PatchFeature(params: ExtraFuncParams, feature: any) {
feature[this._name] = this._f({featuresPerLayer: featuresPerLayer, relations: relations}, feature) 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 * 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 * 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 { export default class LocalStorageSaverActor {
public static readonly storageKey: string = "cached-features"; public static readonly storageKey: string = "cached-features";
@ -21,7 +21,6 @@ export default class LocalStorageSaverActor {
try { try {
localStorage.setItem(key, JSON.stringify(features)); localStorage.setItem(key, JSON.stringify(features));
console.log("Saved ", features.length, "elements to", key)
localStorage.setItem(key + "-time", JSON.stringify(now)) localStorage.setItem(key + "-time", JSON.stringify(now))
} catch (e) { } catch (e) {
console.warn("Could not save the features to local storage:", e) console.warn("Could not save the features to local storage:", e)

View file

@ -1,6 +1,6 @@
import FeatureSource from "./FeatureSource"; import FeatureSource from "../FeatureSource";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import State from "../../State"; import State from "../../../State";
export default class RegisteringAllFromFeatureSourceActor { export default class RegisteringAllFromFeatureSourceActor {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; 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 {UIEventSource} from "../UIEventSource";
import {Changes} from "../Osm/Changes"; import {Changes} from "../Osm/Changes";
import {ChangeDescription} from "../Osm/Actions/ChangeDescription"; import {ChangeDescription} from "../Osm/Actions/ChangeDescription";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject"; 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 * Applies changes from 'Changes' onto a featureSource
@ -12,10 +37,18 @@ import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject";
export default class ChangeApplicator implements FeatureSource { export default class ChangeApplicator implements FeatureSource {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name: string; 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 generateNewGeometries: boolean
}) { }) {
this.source = source;
this.changes = changes;
this.mode = mode;
this.name = "ChangesApplied(" + source.name + ")" this.name = "ChangesApplied(" + source.name + ")"
this.features = source.features this.features = source.features
@ -26,7 +59,7 @@ export default class ChangeApplicator implements FeatureSource {
if (runningUpdate) { if (runningUpdate) {
return; // No need to ping again return; // No need to ping again
} }
ChangeApplicator.ApplyChanges(features, changes.pendingChanges.data, mode) self.ApplyChanges()
seenChanges.clear() seenChanges.clear()
}) })
@ -34,19 +67,20 @@ export default class ChangeApplicator implements FeatureSource {
runningUpdate = true; runningUpdate = true;
changes = changes.filter(ch => !seenChanges.has(ch)) changes = changes.filter(ch => !seenChanges.has(ch))
changes.forEach(c => seenChanges.add(c)) changes.forEach(c => seenChanges.add(c))
ChangeApplicator.ApplyChanges(self.features.data, changes, mode) self.ApplyChanges()
source.features.ping() source.features.ping()
runningUpdate = false; runningUpdate = false;
}) })
} }
/** /**
* Returns true if the geometry is changed and the source should be pinged * 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) { if (cs.length === 0 || features === undefined) {
return; return;
} }
@ -56,12 +90,18 @@ export default class ChangeApplicator implements FeatureSource {
const changesPerId: Map<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>() const changesPerId: Map<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>()
for (const c of cs) { for (const c of cs) {
const id = c.type + "/" + c.id const id = c.type + "/" + c.id
if (!loadedIds.has(id)) {
continue
}
if (!changesPerId.has(id)) { if (!changesPerId.has(id)) {
changesPerId.set(id, []) changesPerId.set(id, [])
} }
changesPerId.get(id).push(c) 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() const now = new Date()
@ -77,7 +117,7 @@ export default class ChangeApplicator implements FeatureSource {
// First, create the new features - they have a negative ID // First, create the new features - they have a negative ID
// We don't set the properties yet though // We don't set the properties yet though
if (mode?.generateNewGeometries) { if (this.mode?.generateNewGeometries) {
changesPerId.forEach(cs => { changesPerId.forEach(cs => {
cs cs
.forEach(change => { .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 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[]>, private readonly overpassUpdater: OverpassFeatureSource
changes: Changes, private readonly relationTracker: RelationsTracker
updater: FeatureSource, private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>;
fromOsmApi: FeatureSource, constructor(
layout: UIEventSource<LayoutConfig>, handleFeatureSource: (source: FeatureSourceForLayer) => void,
locationControl: UIEventSource<Loc>, state: {
selectedElement: UIEventSource<any>) { 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 const perLayerHierarchy = new Map<string, TileHierarchyMerger>()
// Note that we need to register before we do metatagging (as it expects the event sources) this.perLayerHierarchy = perLayerHierarchy
// AT last, the metaTagging also needs to be run _after_ the duplicatorPerLayer const patchedHandleFeatureSource = function (src: FeatureSourceForLayer) {
const amendedOverpassSource = // This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
new RememberingSource( const srcFiltered =
new LocalStorageSaver( new FilteringFeatureSource(state,
new MetaTaggingFeatureSource(allLoadedFeatures, new WayHandlingApplyingFeatureSource(
new FeatureDuplicatorPerLayer(flayers, src,
new RegisteringFeatureSource( )
new ChangeApplicator( )
updater, changes handleFeatureSource(srcFiltered)
)) self.somethingLoaded.setData(true)
)), layout)); };
const geojsonSources: FeatureSource [] = GeoJsonSource function addToHierarchy(src: FeatureSource & Tiled, layerId: string) {
.ConstructMultiSource(flayers.data, locationControl) perLayerHierarchy.get(layerId).registerTile(src)
.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
});
const amendedLocalStorageSource = for (const filteredLayer of state.filteredLayers.data) {
new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new ChangeApplicator(new LocalStorageSource(layout), changes)) 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( if (source.geojsonSource === undefined) {
new MetaTaggingFeatureSource(allLoadedFeatures, // This is an OSM layer
new FeatureDuplicatorPerLayer(flayers, // We load the cached values and register them
new RegisteringFeatureSource(new ChangeApplicator(fromOsmApi, changes, // Getting data from upstream happens a bit lower
{ new TiledFromLocalStorageSource(filteredLayer,
// We lump in the new points here (src) => {
generateNewGeometries: true 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 {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import FilteredLayer from "../../Models/FilteredLayer";
import {BBox} from "../GeoOperations";
export default interface FeatureSource { export default interface FeatureSource {
features: UIEventSource<{ feature: any, freshness: Date }[]>; features: UIEventSource<{ feature: any, freshness: Date }[]>;
@ -9,38 +11,30 @@ export default interface FeatureSource {
name: string; 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 * A feature source which only contains features for the defined layer
*/ */
public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) { export interface FeatureSourceForLayer extends FeatureSource{
let defaults = { readonly layer: FilteredLayer
metadata: false, }
}
options = Utils.setDefaults(options, defaults); /**
* A feature source which is aware of the indexes it contains
// Select all features, ignore the freshness and other data */
let featureList: any[] = featurePipeline.features.data.map((feature) => export interface IndexedFeatureSource extends FeatureSource {
JSON.parse(JSON.stringify((feature.feature)))); // Make a deep copy! readonly containedIds: UIEventSource<Set<string>>
}
if (!options.metadata) {
for (let i = 0; i < featureList.length; i++) { /**
let feature = featureList[i]; * A feature source which has some extra data about it's state
for (let property in feature.properties) { */
if (property[0] == "_" && property !== "_lat" && property !== "_lon") { export interface FeatureSourceState {
delete featureList[i]["properties"][property]; readonly sufficientlyZoomed: UIEventSource<boolean>;
} readonly runningQuery: UIEventSource<boolean>;
} readonly timeout: UIEventSource<number>;
}
}
return {type: "FeatureCollection", features: featureList}
}
} }

View file

@ -1,8 +1,7 @@
import FeatureSource from "./FeatureSource"; import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
import FilteredLayer from "../../Models/FilteredLayer"; import FilteredLayer from "../../Models/FilteredLayer";
import OverpassFeatureSource from "../Actors/OverpassFeatureSource"; import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
import SimpleFeatureSource from "./SimpleFeatureSource";
/** /**
@ -13,17 +12,17 @@ import SimpleFeatureSource from "./SimpleFeatureSource";
export default class PerLayerFeatureSourceSplitter { export default class PerLayerFeatureSourceSplitter {
constructor(layers: UIEventSource<FilteredLayer[]>, constructor(layers: UIEventSource<FilteredLayer[]>,
handleLayerData: (source: FeatureSource) => void, handleLayerData: (source: FeatureSourceForLayer) => void,
upstream: OverpassFeatureSource) { upstream: FeatureSource) {
const knownLayers = new Map<string, FeatureSource>() const knownLayers = new Map<string, FeatureSourceForLayer>()
function update() { function update() {
const features = upstream.features.data; const features = upstream.features.data;
if (features === undefined) { if (features === undefined) {
return; return;
} }
if(layers.data === undefined){ if (layers.data === undefined) {
return; return;
} }
@ -69,19 +68,16 @@ export default class PerLayerFeatureSourceSplitter {
if (featureSource === undefined) { if (featureSource === undefined) {
// Not yet initialized - now is a good time // Not yet initialized - now is a good time
featureSource = new SimpleFeatureSource(layer) featureSource = new SimpleFeatureSource(layer)
featureSource.features.setData(features)
knownLayers.set(id, featureSource) knownLayers.set(id, featureSource)
handleLayerData(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()) 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 * 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 * 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 features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name; public readonly name;
public readonly layer: FilteredLayer public readonly layer: FilteredLayer
private readonly _sources: UIEventSource<FeatureSource[]>; 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._sources = sources;
this.layer = layer; this.layer = layer;
this.name = "SourceMerger" this.name = "FeatureSourceMerger("+layer.layerDef.id+", "+Utils.tile_from_index(tileIndex).join(",")+")"
const self = this; const self = this;
const handledSources = new Set<FeatureSource>(); const handledSources = new Set<FeatureSource>();

View file

@ -1,13 +1,13 @@
import {FeatureSourceForLayer} from "./FeatureSource"; import {UIEventSource} from "../../UIEventSource";
import {UIEventSource} from "../UIEventSource"; import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import Hash from "../Web/Hash"; import FilteredLayer from "../../../Models/FilteredLayer";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import {FeatureSourceForLayer} from "../FeatureSource";
import FilteredLayer from "../../Models/FilteredLayer"; import Hash from "../../Web/Hash";
export default class FilteringFeatureSource implements FeatureSourceForLayer { export default class FilteringFeatureSource implements FeatureSourceForLayer {
public features: UIEventSource<{ feature: any; freshness: Date }[]> = public features: UIEventSource<{ feature: any; freshness: Date }[]> =
new UIEventSource<{ feature: any; freshness: Date }[]>([]); new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name = "FilteringFeatureSource"; public readonly name;
public readonly layer: FilteredLayer; public readonly layer: FilteredLayer;
constructor( constructor(
@ -18,6 +18,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer {
upstream: FeatureSourceForLayer upstream: FeatureSourceForLayer
) { ) {
const self = this; const self = this;
this.name = "FilteringFeatureSource("+upstream.name+")"
this.layer = upstream.layer; this.layer = upstream.layer;
const 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 * 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 features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name; public readonly name;
@ -17,6 +17,8 @@ export default class GeoJsonSource implements FeatureSourceForLayer {
private readonly seenids: Set<string> = new Set<string>() private readonly seenids: Set<string> = new Set<string>()
public readonly layer: FilteredLayer; public readonly layer: FilteredLayer;
public readonly tileIndex
public readonly bbox;
public constructor(flayer: FilteredLayer, public constructor(flayer: FilteredLayer,
zxy?: [number, number, number]) { zxy?: [number, number, number]) {
@ -28,10 +30,16 @@ export default class GeoJsonSource implements FeatureSourceForLayer {
this.layer = flayer; this.layer = flayer;
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id); let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
if (zxy !== undefined) { if (zxy !== undefined) {
const [z, x, y] = zxy;
url = url url = url
.replace('{z}', "" + zxy[0]) .replace('{z}', "" + z)
.replace('{x}', "" + zxy[1]) .replace('{x}', "" + x)
.replace('{y}', "" + zxy[2]) .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; this.name = "GeoJsonSource of " + url;

View file

@ -1,9 +1,9 @@
import FeatureSource from "./FeatureSource"; import FeatureSource from "../FeatureSource";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import {OsmObject} from "../Osm/OsmObject"; import Loc from "../../../Models/Loc";
import {Utils} from "../../Utils"; import FilteredLayer from "../../../Models/FilteredLayer";
import Loc from "../../Models/Loc"; import {Utils} from "../../../Utils";
import FilteredLayer from "../../Models/FilteredLayer"; import {OsmObject} from "../../Osm/OsmObject";
export default class OsmApiFeatureSource implements FeatureSource { 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. * Every previously added point is remembered, but new points are added.
* Data coming from upstream will always overwrite a previous value * Data coming from upstream will always overwrite a previous value
*/ */
import FeatureSource from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
export default class RememberingSource implements FeatureSource { export default class RememberingSource implements FeatureSource {
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>; public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>;

View file

@ -1,6 +1,6 @@
import {FeatureSourceForLayer} from "./FeatureSource"; import {UIEventSource} from "../../UIEventSource";
import {UIEventSource} from "../UIEventSource"; import FilteredLayer from "../../../Models/FilteredLayer";
import FilteredLayer from "../../Models/FilteredLayer"; import {FeatureSourceForLayer} from "../FeatureSource";
export default class SimpleFeatureSource implements FeatureSourceForLayer { export default class SimpleFeatureSource implements FeatureSourceForLayer {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); 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 features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name: string = "StaticFeatureSource" public readonly name: string = "StaticFeatureSource"
constructor(features: any[]) { constructor(features: any[] | UIEventSource<any[]>, useFeaturesDirectly = false) {
const now = new Date(); const now = new Date();
this.features = new UIEventSource(features.map(f => ({ if(useFeaturesDirectly){
feature: f, // @ts-ignore
freshness: now 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) * 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 { export default class WayHandlingApplyingFeatureSource implements FeatureSourceForLayer {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name; public readonly name;
public readonly layer; public readonly layer;
constructor(upstream: FeatureSourceForLayer) { constructor(upstream: FeatureSourceForLayer) {
this.name = "Wayhandling of " + upstream.name; this.name = "Wayhandling(" + upstream.name+")";
this.layer = upstream.layer this.layer = upstream.layer
const layer = upstream.layer.layerDef; 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 State from "../../../State";
import FilteredLayer from "../../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer} from "../FeatureSource"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {Utils} from "../../../Utils"; import {Utils} from "../../../Utils";
import {UIEventSource} from "../../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc"; 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>(); 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( constructor(
layer: FilteredLayer, layer: FilteredLayer,
zoomlevel: number, zoomlevel: number,
constructTile: (xy: [number, number]) => FeatureSourceForLayer, constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled),
state: { state: {
locationControl: UIEventSource<Loc> locationControl: UIEventSource<Loc>
leafletMap: any leafletMap: any
@ -24,6 +26,8 @@ export default class DynamicTileSource {
) { ) {
state = State.state state = State.state
const self = this; const self = this;
this.loadedTiles = new Map<number,FeatureSourceForLayer & Tiled>()
const neededTiles = state.locationControl.map( const neededTiles = state.locationControl.map(
location => { location => {
if (!layer.isDisplayed.data) { if (!layer.isDisplayed.data) {
@ -45,7 +49,7 @@ export default class DynamicTileSource {
const tileRange = Utils.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) 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)) 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 undefined
} }
return needed return needed
@ -53,20 +57,22 @@ export default class DynamicTileSource {
, [layer.isDisplayed, state.leafletMap]).stabilized(250); , [layer.isDisplayed, state.leafletMap]).stabilized(250);
neededTiles.addCallbackAndRunD(neededIndexes => { neededTiles.addCallbackAndRunD(neededIndexes => {
console.log("Tiled geojson source ",layer.layerDef.id," needs", neededIndexes)
if (neededIndexes === undefined) {
return;
}
for (const neededIndex of neededIndexes) { for (const neededIndex of neededIndexes) {
self._loadedTiles.add(neededIndex) self._loadedTiles.add(neededIndex)
const xy = Utils.tile_from_index(zoomlevel, neededIndex) const src = constructTile( Utils.tile_from_index(neededIndex))
const src = constructTile(xy) if(src !== undefined){
let xmap = self.existingTiles.get(xy[0]) self.loadedTiles.set(neededIndex, src)
if(xmap === undefined){
xmap = new Map<number, FeatureSourceForLayer>()
self.existingTiles.set(xy[0], xmap)
} }
xmap.set(xy[1], src)
} }
}) })
} }
} }

View file

@ -1,3 +1,27 @@
Data in MapComplete can come from multiple sources. 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 TileHierarchy from "./TileHierarchy";
import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource"; import {UIEventSource} from "../../UIEventSource";
import {UIEventSource} from "../UIEventSource"; import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import FilteredLayer from "../../Models/FilteredLayer"; import FilteredLayer from "../../../Models/FilteredLayer";
import FeatureSourceMerger from "./Sources/FeatureSourceMerger"; import {Utils} from "../../../Utils";
import {BBox} from "../GeoOperations"; import {BBox} from "../../GeoOperations";
import {Utils} from "../../Utils"; import FeatureSourceMerger from "../Sources/FeatureSourceMerger";
export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> { export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, 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 src
* @param index * @param index
*/ */
public registerTile(src: FeatureSource, index: number) { public registerTile(src: FeatureSource & Tiled) {
const index = src.tileIndex
if (this.sources.has(index)) { if (this.sources.has(index)) {
const sources = this.sources.get(index) const sources = this.sources.get(index)
sources.data.push(src) 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 FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer} from "../FeatureSource"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource"; import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc"; import Loc from "../../../Models/Loc";
import GeoJsonSource from "../GeoJsonSource"; import TileHierarchy from "./TileHierarchy";
import DynamicTileSource from "./DynamicTileSource"; 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, constructor(layer: FilteredLayer,
registerLayer: (layer: FeatureSourceForLayer) => void, handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void,
state: { state: {
locationControl: UIEventSource<Loc> locationControl: UIEventSource<Loc>
leafletMap: any 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( const prefix = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-"
layer, // @ts-ignore
source.geojsonZoomLevel, const indexes: number[] = Object.keys(localStorage)
(xy) => { .filter(key => {
const xyz: [number, number, number] = [xy[0], xy[1], source.geojsonZoomLevel] return key.startsWith(prefix) && !key.endsWith("-time");
const src = new GeoJsonSource( })
layer, .map(key => {
xyz return Number(key.substring(prefix.length));
) })
registerLayer(src)
return src console.log("Avaible datasets in local storage:", indexes)
},
state 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 * as turf from '@turf/turf'
import {Utils} from "../Utils";
export class GeoOperations { export class GeoOperations {
@ -185,6 +186,44 @@ export class GeoOperations {
return turf.length(feature) * 1000 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 * Generates the closest point on a way from a given point
* @param way The road on which you want to find a point * @param way The road on which you want to find a point
@ -340,6 +379,7 @@ export class BBox {
readonly maxLon: number; readonly maxLon: number;
readonly minLat: number; readonly minLat: number;
readonly minLon: number; readonly minLon: number;
static global: BBox = new BBox([[-180,-90],[180,90]]);
constructor(coordinates) { constructor(coordinates) {
this.maxLat = Number.MIN_VALUE; this.maxLat = Number.MIN_VALUE;
@ -361,12 +401,11 @@ export class BBox {
return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]]) return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]])
} }
static get(feature) { static get(feature): BBox {
if (feature.bbox?.overlapsWith === undefined) { if (feature.bbox?.overlapsWith === undefined) {
const turfBbox: number[] = turf.bbox(feature) const turfBbox: number[] = turf.bbox(feature)
feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]); feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]);
} }
return feature.bbox; 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\/(.*)/) const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/)
console.log("Mapview matched ", value, mapview)
if(mapview !== null){ if(mapview !== null){
const key = mapview[1] const key = mapview[1]
return {key:key, isApiv4: !isNaN(Number(key))}; return {key:key, isApiv4: !isNaN(Number(key))};

View file

@ -1,15 +1,9 @@
import SimpleMetaTagger from "./SimpleMetaTagger"; import SimpleMetaTagger from "./SimpleMetaTagger";
import {ExtraFunction} from "./ExtraFunction"; import {ExtraFuncParams, ExtraFunction} from "./ExtraFunction";
import {Relation} from "./Osm/ExtractRelations";
import {UIEventSource} from "./UIEventSource"; import {UIEventSource} from "./UIEventSource";
import LayerConfig from "../Models/ThemeConfig/LayerConfig"; 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, ... * 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; private static readonly stopErrorOutputAt = 10;
/** /**
* An actor which adds metatags on every feature in the given object * This method (re)calculates all metatags and calculated tags on every given object.
* The features are a list of geojson-features, with a "properties"-field and geometry * The given features should be part of the given layer
*/ */
static addMetatags(features: { feature: any; freshness: Date }[], static addMetatags(features: { feature: any; freshness: Date }[],
allKnownFeatures: UIEventSource<{ feature: any; freshness: Date }[]>, params: ExtraFuncParams,
relations: Map<string, { role: string, relation: Relation }[]>, layer: LayerConfig,
layers: LayerConfig[],
includeDates = true) { includeDates = true) {
if (features === undefined || features.length === 0) { if (features === undefined || features.length === 0) {
@ -44,59 +37,32 @@ export default class MetaTagging {
metatag.addMetaTags(features); metatag.addMetaTags(features);
} catch (e) { } catch (e) {
console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e) console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e)
} }
} }
// The functions - per layer - which add the new keys // The functions - per layer - which add the new keys
const layerFuncs = new Map<string, ((params: Params, feature: any) => void)>(); const layerFuncs = this.createRetaggingFunc(layer)
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)
}
if (layerFuncs !== undefined) {
for (const feature of features) { for (const feature of features) {
// @ts-ignore
const key = feature.feature._matching_layer_id;
const f = layerFuncs.get(key);
if (f === undefined) {
continue;
}
try { try {
f({featuresPerLayer: featuresPerLayer, memberships: relations}, feature.feature) layerFuncs(params, feature.feature)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
} }
}
})
} }
private static createRetaggingFunc(layer: LayerConfig): private static createRetaggingFunc(layer: LayerConfig):
((params: Params, feature: any) => void) { ((params: ExtraFuncParams, feature: any) => void) {
const calculatedTags: [string, string][] = layer.calculatedTags; const calculatedTags: [string, string][] = layer.calculatedTags;
if (calculatedTags === undefined) { if (calculatedTags === undefined) {
return undefined; return undefined;
} }
const functions: ((params: Params, feature: any) => void)[] = []; const functions: ((params: ExtraFuncParams, feature: any) => void)[] = [];
for (const entry of calculatedTags) { for (const entry of calculatedTags) {
const key = entry[0] const key = entry[0]
const code = entry[1]; const code = entry[1];
@ -145,14 +111,13 @@ export default class MetaTagging {
console.error("Could not create a dynamic function: ", e) console.error("Could not create a dynamic function: ", e)
} }
} }
return (params: Params, feature) => { return (params: ExtraFuncParams, feature) => {
const tags = feature.properties const tags = feature.properties
if (tags === undefined) { if (tags === undefined) {
return; return;
} }
const relations = params.memberships?.get(feature.properties.id) ?? [] ExtraFunction.FullPatchFeature(params, feature);
ExtraFunction.FullPatchFeature(params.featuresPerLayer, relations, feature);
try { try {
for (const f of functions) { for (const f of functions) {
f(params, feature); f(params, feature);

View file

@ -1,15 +1,30 @@
/**
* Represents a single change to an object
*/
export interface ChangeDescription { export interface ChangeDescription {
/**
* Identifier of the object
*/
type: "node" | "way" | "relation", type: "node" | "way" | "relation",
/** /**
* Negative for a new objects * Identifier of the object
* Negative for new objects
*/ */
id: number, 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 }[], 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?: { changes?: {
lat: number, lat: number,
lon: number lon: number

View file

@ -11,7 +11,7 @@ export class Geocoding {
osm_type: string, osm_id: string osm_type: string, osm_id: string
}[]) => void), }[]) => void),
onFail: (() => 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=" + const url = Geocoding.host + "format=json&limit=1&viewbox=" +
`${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` + `${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}` +
"&accept-language=nl&q=" + query; "&accept-language=nl&q=" + query;

View file

@ -1,7 +1,7 @@
import * as OsmToGeoJson from "osmtogeojson"; import * as OsmToGeoJson from "osmtogeojson";
import Bounds from "../../Models/Bounds"; import Bounds from "../../Models/Bounds";
import {TagsFilter} from "../Tags/TagsFilter"; import {TagsFilter} from "../Tags/TagsFilter";
import ExtractRelations from "./ExtractRelations"; import RelationsTracker from "./RelationsTracker";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import {UIEventSource} from "../UIEventSource"; import {UIEventSource} from "../UIEventSource";
@ -15,16 +15,20 @@ export class Overpass {
private readonly _timeout: UIEventSource<number>; private readonly _timeout: UIEventSource<number>;
private readonly _extraScripts: string[]; private readonly _extraScripts: string[];
private _includeMeta: boolean; private _includeMeta: boolean;
private _relationTracker: RelationsTracker;
constructor(filter: TagsFilter, extraScripts: string[], constructor(filter: TagsFilter, extraScripts: string[],
interpreterUrl: UIEventSource<string>, interpreterUrl: UIEventSource<string>,
timeout: UIEventSource<number>, timeout: UIEventSource<number>,
relationTracker: RelationsTracker,
includeMeta = true) { includeMeta = true) {
this._timeout = timeout; this._timeout = timeout;
this._interpreterUrl = interpreterUrl; this._interpreterUrl = interpreterUrl;
this._filter = filter this._filter = filter
this._extraScripts = extraScripts; this._extraScripts = extraScripts;
this._includeMeta = includeMeta; this._includeMeta = includeMeta;
this._relationTracker = relationTracker
} }
queryGeoJson(bounds: Bounds, continuation: ((any, date: Date) => void), onFail: ((reason) => void)): void { queryGeoJson(bounds: Bounds, continuation: ((any, date: Date) => void), onFail: ((reason) => void)): void {
@ -35,6 +39,7 @@ export class Overpass {
console.log("Using testing URL") console.log("Using testing URL")
query = Overpass.testUrl; query = Overpass.testUrl;
} }
const self = this;
Utils.downloadJson(query) Utils.downloadJson(query)
.then(json => { .then(json => {
if (json.elements === [] && ((json.remarks ?? json.remark).indexOf("runtime error") >= 0)) { 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 // @ts-ignore
const geojson = OsmToGeoJson.default(json); const geojson = OsmToGeoJson.default(json);
const osmTime = new Date(json.osm3s.timestamp_osm_base); const osmTime = new Date(json.osm3s.timestamp_osm_base);
continuation(geojson, osmTime); continuation(geojson, osmTime);
}).catch(onFail) }).catch(e => {
onFail(e);
})
} }
buildQuery(bbox: string): string { buildQuery(bbox: string): string {

View file

@ -1,4 +1,5 @@
import State from "../../State"; import State from "../../State";
import {UIEventSource} from "../UIEventSource";
export interface Relation { export interface Relation {
id: number, id: number,
@ -13,11 +14,15 @@ export interface Relation {
properties: any properties: any
} }
export default class ExtractRelations { export default class RelationsTracker {
public static RegisterRelations(overpassJson: any): void { public knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(new Map(), "Relation memberships");
const memberships = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(overpassJson))
State.state.knownRelations.setData(memberships) constructor() {
}
public RegisterRelations(overpassJson: any): void {
this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson))
} }
/** /**
@ -25,7 +30,7 @@ export default class ExtractRelations {
* @param overpassJson * @param overpassJson
* @constructor * @constructor
*/ */
public static GetRelationElements(overpassJson: any): Relation[] { private static GetRelationElements(overpassJson: any): Relation[] {
const relations = overpassJson.elements const relations = overpassJson.elements
.filter(element => element.type === "relation" && element.tags.type !== "multipolygon") .filter(element => element.type === "relation" && element.tags.type !== "multipolygon")
for (const relation of relations) { for (const relation of relations) {
@ -39,12 +44,11 @@ export default class ExtractRelations {
* @param relations * @param relations
* @constructor * @constructor
*/ */
public static BuildMembershipTable(relations: Relation[]): Map<string, { role: string, relation: Relation }[]> { private UpdateMembershipTable(relations: Relation[]): void {
const memberships = new Map<string, { role: string, relation: Relation }[]>() const memberships = this.knownRelations.data
let changed = false;
for (const relation of relations) { for (const relation of relations) {
for (const member of relation.members) { for (const member of relation.members) {
const role = { const role = {
role: member.role, role: member.role,
relation: relation relation: relation
@ -53,11 +57,21 @@ export default class ExtractRelations {
if (!memberships.has(key)) { if (!memberships.has(key)) {
memberships.set(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 BaseUIElement from "../UI/BaseUIElement";
import Title from "../UI/Base/Title"; import Title from "../UI/Base/Title";
import {FixedUiElement} from "../UI/Base/FixedUiElement"; import {FixedUiElement} from "../UI/Base/FixedUiElement";
import CountryCoder from "latlon2country/index";
const cardinalDirections = { const cardinalDirections = {
@ -20,7 +21,7 @@ const cardinalDirections = {
export default class SimpleMetaTagger { export default class SimpleMetaTagger {
static coder: any; private static coder: CountryCoder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
public static readonly objectMetaInfo = new SimpleMetaTagger( public static readonly objectMetaInfo = new SimpleMetaTagger(
{ {
keys: ["_last_edit:contributor", keys: ["_last_edit:contributor",
@ -84,7 +85,7 @@ export default class SimpleMetaTagger {
}, },
(feature => { (feature => {
const units = Utils.NoNull([].concat(...State.state?.layoutToUse?.data?.layers?.map(layer => layer.units ?? []))); const units = Utils.NoNull([].concat(...State.state?.layoutToUse?.data?.layers?.map(layer => layer.units ?? [])));
if(units.length == 0){ if (units.length == 0) {
return; return;
} }
let rewritten = false; let rewritten = false;
@ -93,7 +94,7 @@ export default class SimpleMetaTagger {
continue; continue;
} }
for (const unit of units) { for (const unit of units) {
if(unit.appliesToKeys === undefined){ if (unit.appliesToKeys === undefined) {
console.error("The unit ", unit, "has no appliesToKey defined") console.error("The unit ", unit, "has no appliesToKey defined")
continue continue
} }
@ -148,7 +149,7 @@ export default class SimpleMetaTagger {
const lat = centerPoint.geometry.coordinates[1]; const lat = centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0]; const lon = centerPoint.geometry.coordinates[0];
SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => { SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, (countries: string[]) => {
try { try {
const oldCountry = feature.properties["_country"]; const oldCountry = feature.properties["_country"];
feature.properties["_country"] = countries[0].trim().toLowerCase(); feature.properties["_country"] = countries[0].trim().toLowerCase();
@ -160,7 +161,7 @@ export default class SimpleMetaTagger {
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
} }
}); })
} }
) )
private static isOpen = new SimpleMetaTagger( private static isOpen = new SimpleMetaTagger(
@ -426,11 +427,7 @@ export default class SimpleMetaTagger {
} }
} }
static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) { public static HelpText(): BaseUIElement {
SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, callback)
}
static HelpText(): BaseUIElement {
const subElements: (string | BaseUIElement)[] = [ const subElements: (string | BaseUIElement)[] = [
new Combine([ new Combine([
new Title("Metatags", 1), new Title("Metatags", 1),
@ -453,7 +450,7 @@ export default class SimpleMetaTagger {
return new Combine(subElements).SetClass("flex-col") 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++) { for (let i = 0; i < features.length; i++) {
let feature = features[i]; let feature = features[i];
this._f(feature.feature, i, feature.freshness); this._f(feature.feature, i, feature.freshness);

View file

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

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants { 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 // The user journey states thresholds when a new feature gets unlocked
public static userJourney = { public static userJourney = {
@ -26,12 +26,6 @@ export default class Constants {
*/ */
static updateTimeoutSec: number = 30; 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 { private static isRetina(): boolean {
if (Utils.runningFromConsole) { if (Utils.runningFromConsole) {
return; return;

View file

@ -59,9 +59,8 @@ export interface LayerConfigJson {
* NOTE: the previous format was 'overpassTags: AndOrTagConfigJson | string', which is interpreted as a shorthand for source: {osmTags: "key=value"} * 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 * While still supported, this is considered deprecated
*/ */
source: { osmTags: AndOrTagConfigJson | string } | source: { osmTags: AndOrTagConfigJson | string, overpassScript?: string } |
{ osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean } | { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean }
{ osmTags: AndOrTagConfigJson | string, overpassScript: string }
/** /**
* *

View file

@ -246,14 +246,6 @@ export default class LayoutConfig {
return icons return icons
} }
public LayerIndex(): Map<string, LayerConfig> {
const index = new Map<string, LayerConfig>();
for (const layer of this.layers) {
index.set(layer.id, layer)
}
return index;
}
/** /**
* Replaces all the relative image-urls with a fixed image url * Replaces all the relative image-urls with a fixed image url
* This is to fix loading from external sources * This is to fix loading from external sources

View file

@ -2,18 +2,18 @@ import {TagsFilter} from "../../Logic/Tags/TagsFilter";
export default class SourceConfig { export default class SourceConfig {
osmTags?: TagsFilter; public readonly osmTags?: TagsFilter;
overpassScript?: string; public readonly overpassScript?: string;
geojsonSource?: string; public readonly geojsonSource?: string;
geojsonZoomLevel?: number; public readonly geojsonZoomLevel?: number;
isOsmCacheLayer: boolean; public readonly isOsmCacheLayer: boolean;
constructor(params: { constructor(params: {
osmTags?: TagsFilter, osmTags?: TagsFilter,
overpassScript?: string, overpassScript?: string,
geojsonSource?: string, geojsonSource?: string,
isOsmCache?: boolean, isOsmCache?: boolean,
geojsonSourceLevel?: number geojsonSourceLevel?: number,
}, context?: string) { }, context?: string) {
let defined = 0; let defined = 0;

View file

@ -11,16 +11,14 @@ import InstalledThemes from "./Logic/Actors/InstalledThemes";
import BaseLayer from "./Models/BaseLayer"; import BaseLayer from "./Models/BaseLayer";
import Loc from "./Models/Loc"; import Loc from "./Models/Loc";
import Constants from "./Models/Constants"; import Constants from "./Models/Constants";
import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource";
import TitleHandler from "./Logic/Actors/TitleHandler"; import TitleHandler from "./Logic/Actors/TitleHandler";
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
import {Relation} from "./Logic/Osm/ExtractRelations"; import OsmApiFeatureSource from "./Logic/FeatureSource/Sources/OsmApiFeatureSource";
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
import FilteredLayer from "./Models/FilteredLayer"; import FilteredLayer from "./Models/FilteredLayer";
import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor";
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
import {BBox} from "./Logic/GeoOperations";
/** /**
* Contains the global state: a bunch of UI-event sources * Contains the global state: a bunch of UI-event sources
@ -57,8 +55,6 @@ export default class State {
public favouriteLayers: UIEventSource<string[]>; public favouriteLayers: UIEventSource<string[]>;
public layerUpdater: OverpassFeatureSource;
public osmApiFeatureSource: OsmApiFeatureSource; public osmApiFeatureSource: OsmApiFeatureSource;
public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers"); public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([], "filteredLayers");
@ -71,12 +67,6 @@ export default class State {
"Selected element" "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<Map<string, { role: string; relation: Relation }[]>>(undefined, "Relation memberships");
public readonly featureSwitchUserbadge: UIEventSource<boolean>; public readonly featureSwitchUserbadge: UIEventSource<boolean>;
public readonly featureSwitchSearch: UIEventSource<boolean>; public readonly featureSwitchSearch: UIEventSource<boolean>;
public readonly featureSwitchBackgroundSlection: UIEventSource<boolean>; public readonly featureSwitchBackgroundSlection: UIEventSource<boolean>;
@ -96,6 +86,7 @@ export default class State {
public readonly featureSwitchExportAsPdf: UIEventSource<boolean>; public readonly featureSwitchExportAsPdf: UIEventSource<boolean>;
public readonly overpassUrl: UIEventSource<string>; public readonly overpassUrl: UIEventSource<string>;
public readonly overpassTimeout: UIEventSource<number>; public readonly overpassTimeout: UIEventSource<number>;
public readonly overpassMaxZoom: UIEventSource<number> = new UIEventSource<number>(undefined);
public featurePipeline: FeaturePipeline; public featurePipeline: FeaturePipeline;
@ -104,6 +95,12 @@ export default class State {
* The map location: currently centered lat, lon and zoom * The map location: currently centered lat, lon and zoom
*/ */
public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl"); public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl");
/**
* The current visible extent of the screen
*/
public readonly currentBounds = new UIEventSource<BBox>(undefined)
public backgroundLayer; public backgroundLayer;
public readonly backgroundLayerId: UIEventSource<string>; public readonly backgroundLayerId: UIEventSource<string>;
@ -398,7 +395,7 @@ export default class State {
new ChangeToElementsActor(this.changes, this.allElements) new ChangeToElementsActor(this.changes, this.allElements)
this.osmApiFeatureSource = new OsmApiFeatureSource(Constants.useOsmApiAt, this) this.osmApiFeatureSource = new OsmApiFeatureSource(this)
new PendingChangesUploader(this.changes, this.selectedElement); new PendingChangesUploader(this.changes, this.selectedElement);

View file

@ -10,6 +10,9 @@ export default class Img extends BaseUIElement {
fallbackImage?: string fallbackImage?: string
}) { }) {
super(); super();
if(src === undefined || src === "undefined"){
throw "Undefined src for image"
}
this._src = src; this._src = src;
this._rawSvg = rawSvg; this._rawSvg = rawSvg;
this._options = options; this._options = options;

View file

@ -1,208 +1,30 @@
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import * as L from "leaflet";
import {Map} from "leaflet";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer"; import BaseLayer from "../../Models/BaseLayer";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import {BBox} from "../../Logic/GeoOperations";
import {Utils} from "../../Utils"; import {UIEventSource} from "../../Logic/UIEventSource";
export interface MinimapOptions {
background?: UIEventSource<BaseLayer>,
location?: UIEventSource<Loc>,
bounds?: UIEventSource<BBox>,
allowMoving?: boolean,
leafletOptions?: any,
attribution?: BaseUIElement | boolean,
onFullyLoaded?: (leaflet: L.Map) => void,
leafletMap?: UIEventSource<any>,
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
*/
/**
* Construct a minimap
*/
public static createMiniMap: (options: MinimapOptions) => BaseUIElement & { readonly leafletMap: UIEventSource<any> }
export default class Minimap extends BaseUIElement {
private static _nextId = 0;
public readonly leafletMap: UIEventSource<Map>
private readonly _id: string;
private readonly _background: UIEventSource<BaseLayer>;
private readonly _location: UIEventSource<Loc>;
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 }>;
constructor(options?: {
background?: UIEventSource<BaseLayer>,
location?: UIEventSource<Loc>,
allowMoving?: boolean,
leafletOptions?: any,
attribution?: BaseUIElement | boolean,
onFullyLoaded?: (leaflet: L.Map) => void,
leafletMap?: UIEventSource<Map>,
lastClickLocation?: UIEventSource<{ lat: number, lon: number }>
}
) {
super()
options = options ?? {}
this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
this._background = options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
this._location = options?.location ?? new UIEventSource<Loc>({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++
}
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(
"<span id='leaflet-attribution'></span>");
}
}
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)
}
} }

View file

@ -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<Map>
private readonly _id: string;
private readonly _background: UIEventSource<BaseLayer>;
private readonly _location: UIEventSource<Loc>;
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<BBox> | undefined;
private constructor(options: MinimapOptions) {
super()
options = options ?? {}
this.leafletMap = options.leafletMap ?? new UIEventSource<Map>(undefined)
this._background = options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
this._location = options?.location ?? new UIEventSource<Loc>({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(
"<span id='leaflet-attribution'></span>");
}
}
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)
}
}

View file

@ -32,7 +32,7 @@ export default class AllDownloads extends ScrollableFullScreen {
freeDivId: "belowmap", freeDivId: "belowmap",
background: State.state.backgroundLayer, background: State.state.backgroundLayer,
location: State.state.locationControl, location: State.state.locationControl,
features: State.state.featurePipeline.features, features: State.state.featurePipeline,
layout: State.state.layoutToUse, layout: State.state.layoutToUse,
}).isRunning.addCallbackAndRun(isRunning => isExporting.setData(isRunning)) }).isRunning.addCallbackAndRun(isRunning => isExporting.setData(isRunning))
} }

View file

@ -5,19 +5,19 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import UserDetails from "../../Logic/Osm/OsmConnection"; import UserDetails from "../../Logic/Osm/OsmConnection";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import * as L from "leaflet"
import {VariableUiElement} from "../Base/VariableUIElement"; import {VariableUiElement} from "../Base/VariableUIElement";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {BBox} from "../../Logic/GeoOperations";
/** /**
* The bottom right attribution panel in the leaflet map * The bottom right attribution panel in the leaflet map
*/ */
export default class Attribution extends Combine { export default class Attribution extends Combine {
constructor(location: UIEventSource<Loc>, constructor(location: UIEventSource<Loc>,
userDetails: UIEventSource<UserDetails>, userDetails: UIEventSource<UserDetails>,
layoutToUse: UIEventSource<LayoutConfig>, layoutToUse: UIEventSource<LayoutConfig>,
leafletMap: UIEventSource<L.Map>) { currentBounds: UIEventSource<BBox>) {
const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true); 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); 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) { if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
return undefined; return undefined;
} }
const bounds: any = leafletMap?.data?.getBounds(); const bounds: any = currentBounds.data;
if (bounds === undefined) { if (bounds === undefined) {
return 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}` 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); return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true);
}, },
[location, leafletMap] [location, currentBounds]
) )
) )
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]); super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]);

View file

@ -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.maintainer ?? "") == "") ? "" : Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.data.maintainer}),
layoutToUse.data.credits, layoutToUse.data.credits,
"<br/>", "<br/>",
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),
"<br/>", "<br/>",
new VariableUiElement(contributions.map(contributions => { new VariableUiElement(contributions.map(contributions => {
if(contributions === undefined){
return ""
}
const sorted = Array.from(contributions, ([name, value]) => ({ const sorted = Array.from(contributions, ([name, value]) => ({
name, name,
value value

View file

@ -2,54 +2,113 @@ import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg"; import Svg from "../../Svg";
import Translations from "../i18n/Translations"; import Translations from "../i18n/Translations";
import State from "../../State"; import State from "../../State";
import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import CheckBoxes from "../Input/Checkboxes"; import CheckBoxes from "../Input/Checkboxes";
import {GeoOperations} from "../../Logic/GeoOperations"; import {BBox, GeoOperations} from "../../Logic/GeoOperations";
import Toggle from "../Input/Toggle"; import Toggle from "../Input/Toggle";
import Title from "../Base/Title"; 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 { export class DownloadPanel extends Toggle {
constructor() { constructor() {
const state: {
featurePipeline: FeaturePipeline,
layoutToUse: UIEventSource<LayoutConfig>,
currentBounds: UIEventSource<BBox>
} = State.state
const t = Translations.t.general.download 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 includeMetaToggle = new CheckBoxes([t.includeMetaData.Clone()])
const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0) const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0)
const buttonGeoJson = new SubtleButton(Svg.floppy_ui(), const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
new Combine([t.downloadGeojson.Clone().SetClass("font-bold"), new Combine([t.downloadGeojson.Clone().SetClass("font-bold"),
t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col")) t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col"))
.onClick(() => { .onClick(() => {
const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data}) const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
const name = State.state.layoutToUse.data.id; Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "),
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson),
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, { `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, {
mimetype: "application/vnd.geo+json" mimetype: "application/vnd.geo+json"
}); });
}) })
const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine( const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine(
[t.downloadCSV.Clone().SetClass("font-bold"), [t.downloadCSV.Clone().SetClass("font-bold"),
t.downloadCSVHelper.Clone()]).SetClass("flex flex-col")) t.downloadCSVHelper.Clone()]).SetClass("flex flex-col"))
.onClick(() => { .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 csv = GeoOperations.toCSV(geojson.features)
const name = State.state.layoutToUse.data.id;
Utils.offerContentsAsDownloadableFile(csv, Utils.offerContentsAsDownloadableFile(csv,
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, { `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, {
mimetype: "text/csv" mimetype: "text/csv"
}); });
}) })
const downloadButtons = new Combine( 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") .SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
super( super(
downloadButtons, downloadButtons,
t.noDataLoaded.Clone(), t.noDataLoaded.Clone(),
somethingLoaded) state.featurePipeline.somethingLoaded)
}
private static getCleanGeoJson(state: {
featurePipeline: FeaturePipeline,
currentBounds: UIEventSource<BBox>
}, 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
}
} }
} }

View file

@ -15,6 +15,7 @@ import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import Toggle from "../Input/Toggle"; import Toggle from "../Input/Toggle";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Utils} from "../../Utils";
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
@ -62,9 +63,15 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown) const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)
const tabsWithAboutMc = [...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({ tabsWithAboutMc.push({
header: Svg.help, header: Svg.help,
content: new Combine([Translations.t.general.aboutMapcomplete.Clone(), "<br/>Version " + Constants.vNumber]) content: new Combine([Translations.t.general.aboutMapcomplete.Clone()
.Subs({"osmcha_link": osmcha_link}), "<br/>Version " + Constants.vNumber])
.SetClass("link-underline") .SetClass("link-underline")
} }
); );

View file

@ -52,7 +52,7 @@ export default class ImportButton extends Toggle {
const withLoadingCheck = new Toggle( const withLoadingCheck = new Toggle(
t.stillLoading, t.stillLoading,
new Combine([button, appliedTags]).SetClass("flex flex-col"), new Combine([button, appliedTags]).SetClass("flex flex-col"),
State.state.layerUpdater.runningQuery State.state.featurePipeline.runningQuery
) )
super(t.hasBeenImported, withLoadingCheck, isImported) super(t.hasBeenImported, withLoadingCheck, isImported)
} }

View file

@ -9,18 +9,21 @@ import MapControlButton from "../MapControlButton";
import Svg from "../../Svg"; import Svg from "../../Svg";
import AllDownloads from "./AllDownloads"; import AllDownloads from "./AllDownloads";
import FilterView from "./FilterView"; 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 { export default class LeftControls extends Combine {
constructor(featureSource: FeatureSource) { constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc>}) {
const toggledCopyright = new ScrollableFullScreen( const toggledCopyright = new ScrollableFullScreen(
() => Translations.t.general.attribution.attributionTitle.Clone(), () => Translations.t.general.attribution.attributionTitle.Clone(),
() => () =>
new AttributionPanel( new AttributionPanel(
State.state.layoutToUse, State.state.layoutToUse,
new ContributorCount(featureSource).Contributors new ContributorCount(state).Contributors
), ),
undefined undefined
); );

View file

@ -65,10 +65,6 @@ export default class SimpleAddUI extends Toggle {
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
newElementAction.newElementId newElementAction.newElementId
)) ))
console.log("Did set selected element to", State.state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
} }
const addUi = new VariableUiElement( const addUi = new VariableUiElement(
@ -104,7 +100,7 @@ export default class SimpleAddUI extends Toggle {
new Toggle( new Toggle(
Translations.t.general.add.stillLoading.Clone().SetClass("alert"), Translations.t.general.add.stillLoading.Clone().SetClass("alert"),
addUi, addUi,
State.state.layerUpdater.runningQuery State.state.featurePipeline.runningQuery
), ),
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"), Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints) 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 ?? []); const tags = TagUtils.KVtoProperties(preset.tags ?? []);
console.log("Opening precise input ", preset.preciseInput, "with tags", tags)
preciseInput = new LocationInput({ preciseInput = new LocationInput({
mapBackground: backgroundLayer, mapBackground: backgroundLayer,
centerLocation: locationSrc, centerLocation: locationSrc,
@ -215,10 +210,7 @@ export default class SimpleAddUI extends Toggle {
const disableFiltersOrConfirm = new Toggle( const disableFiltersOrConfirm = new Toggle(
openLayerOrConfirm, openLayerOrConfirm,
disableFilter, disableFilter,
preset.layerToAddTo.appliedFilters.map(filters => { preset.layerToAddTo.appliedFilters.map(filters => filters === undefined || filters.normalize().and.length === 0)
console.log("Current filters are ", filters)
return filters === undefined || filters.normalize().and.length === 0;
})
) )

View file

@ -6,7 +6,7 @@ export default class CenterMessageBox extends VariableUiElement {
constructor() { constructor() {
const state = State.state; const state = State.state;
const updater = State.state.layerUpdater; const updater = State.state.featurePipeline;
const t = Translations.t.centerMessage; const t = Translations.t.centerMessage;
const message = updater.runningQuery.map( const message = updater.runningQuery.map(
isRunning => { isRunning => {

View file

@ -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 screenshoter to take png screenshot
* Creates jspdf and downloads it * Creates jspdf and downloads it
@ -8,21 +24,6 @@
* - add new layout in "PDFLayout" * - add new layout in "PDFLayout"
* -> in there are more instructions * -> 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 { export default class ExportPDF {
// dimensions of the map in milimeter // dimensions of the map in milimeter
public isRunning = new UIEventSource(true) public isRunning = new UIEventSource(true)
@ -39,7 +40,7 @@ export default class ExportPDF {
freeDivId: string, freeDivId: string,
location: UIEventSource<Loc>, location: UIEventSource<Loc>,
background?: UIEventSource<BaseLayer> background?: UIEventSource<BaseLayer>
features: UIEventSource<{ feature: any }[]>, features: FeaturePipeline,
layout: UIEventSource<LayoutConfig> layout: UIEventSource<LayoutConfig>
} }
) { ) {
@ -57,7 +58,7 @@ export default class ExportPDF {
zoom: l.zoom + 1 zoom: l.zoom + 1
} }
const minimap = new Minimap({ const minimap = Minimap.createMiniMap({
location: new UIEventSource<Loc>(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 location: new UIEventSource<Loc>(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, background: options.background,
allowMoving: false, allowMoving: false,
@ -83,24 +84,21 @@ export default class ExportPDF {
minimap.AttachTo(options.freeDivId) minimap.AttachTo(options.freeDivId)
// Next: we prepare the features. Only fully contained features are shown // Next: we prepare the features. Only fully contained features are shown
const bounded = options.features.map(feats => { minimap.leafletMap .addCallbackAndRunD(leaflet => {
const leaflet = minimap.leafletMap.data;
if (leaflet === undefined) {
return feats
}
const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2)) const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2))
return feats.filter(f => BBox.get(f.feature).isContainedIn(bounds)) options.features.GetTilesPerLayerWithin(bounds, tile => {
console.log("REndering", tile.name)
new ShowDataLayer(
{
features: tile,
leafletMap: minimap.leafletMap,
layerToShow: tile.layer.layerDef,
enablePopups: false
}
)
})
}, [minimap.leafletMap]) })
// Add the features to the minimap
new ShowDataLayer(
bounded,
minimap.leafletMap,
options.layout,
false
)
} }

View file

@ -4,6 +4,7 @@ import Img from "../Base/Img";
import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource"; import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource";
import BaseUIElement from "../BaseUIElement"; import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement"; import {VariableUiElement} from "../Base/VariableUIElement";
import Loading from "../Base/Loading";
export class AttributedImage extends Combine { export class AttributedImage extends Combine {
@ -16,8 +17,13 @@ export class AttributedImage extends Combine {
img = new Img(urlSource); img = new Img(urlSource);
attr = new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon()) attr = new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon())
} else { } else {
img = new VariableUiElement(preparedUrl.map(url => new Img(url, false, {fallbackImage: './assets/svg/blocked.svg'}))) img = new VariableUiElement(preparedUrl.map(url => {
attr = new VariableUiElement(preparedUrl.map(url => new Attribution(imgSource.GetAttributionFor(urlSource), imgSource.SourceIcon()))) 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())))
} }

View file

@ -6,13 +6,13 @@ import BaseUIElement from "../BaseUIElement";
import {FixedUiElement} from "../Base/FixedUiElement"; import {FixedUiElement} from "../Base/FixedUiElement";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import Minimap from "../Base/Minimap";
/** /**
* Selects a direction in degrees * Selects a direction in degrees
*/ */
export default class DirectionInput extends InputElement<string> { export default class DirectionInput extends InputElement<string> {
public static constructMinimap: ((any) => BaseUIElement);
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _location: UIEventSource<Loc>; private readonly _location: UIEventSource<Loc>;
private readonly value: UIEventSource<string>; private readonly value: UIEventSource<string>;
@ -40,7 +40,7 @@ export default class DirectionInput extends InputElement<string> {
let map: BaseUIElement = new FixedUiElement("") let map: BaseUIElement = new FixedUiElement("")
if (!Utils.runningFromConsole) { if (!Utils.runningFromConsole) {
map = DirectionInput.constructMinimap({ map = Minimap.createMiniMap({
background: this.background, background: this.background,
allowMoving: false, allowMoving: false,
location: this._location location: this._location

View file

@ -5,7 +5,7 @@ import Svg from "../../Svg";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import {GeoOperations} from "../../Logic/GeoOperations"; import {GeoOperations} from "../../Logic/GeoOperations";
import DirectionInput from "./DirectionInput"; import Minimap from "../Base/Minimap";
/** /**
@ -41,7 +41,7 @@ export default class LengthInput extends InputElement<string> {
// @ts-ignore // @ts-ignore
let map = undefined let map = undefined
if (!Utils.runningFromConsole) { if (!Utils.runningFromConsole) {
map = DirectionInput.constructMinimap({ map = Minimap.createMiniMap({
background: this.background, background: this.background,
allowMoving: false, allowMoving: false,
location: this._location, location: this._location,

View file

@ -8,35 +8,31 @@ import Svg from "../../Svg";
import State from "../../State"; import State from "../../State";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {GeoOperations} from "../../Logic/GeoOperations"; import {GeoOperations} from "../../Logic/GeoOperations";
import ShowDataLayer from "../ShowDataLayer"; import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import * as L from "leaflet"; 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<Loc> { export default class LocationInput extends InputElement<Loc> {
private static readonly matchLayout = new UIEventSource(new LayoutConfig({ private static readonly matchLayer = new LayerConfig(
description: "Matchpoint style", {
icon: "./assets/svg/crosshair-empty.svg",
id: "matchpoint",
language: ["en"],
layers: [{
id: "matchpoint", source: { id: "matchpoint", source: {
osmTags: {and: []} osmTags: {and: []}
}, },
icon: "./assets/svg/crosshair-empty.svg" icon: "./assets/svg/crosshair-empty.svg"
}], }, "matchpoint icon", true
maintainer: "MapComplete", )
startLat: 0,
startLon: 0,
startZoom: 0,
title: "Location input",
version: "0"
}));
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined) public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined)
private _centerLocation: UIEventSource<Loc>; private _centerLocation: UIEventSource<Loc>;
private readonly mapBackground: UIEventSource<BaseLayer>; private readonly mapBackground: UIEventSource<BaseLayer>;
/**
* The features to which the input should be snapped
* @private
*/
private readonly _snapTo: UIEventSource<{ feature: any }[]> private readonly _snapTo: UIEventSource<{ feature: any }[]>
private readonly _value: UIEventSource<Loc> private readonly _value: UIEventSource<Loc>
private readonly _snappedPoint: UIEventSource<any> private readonly _snappedPoint: UIEventSource<any>
@ -143,7 +139,7 @@ export default class LocationInput extends InputElement<Loc> {
protected InnerConstructElement(): HTMLElement { protected InnerConstructElement(): HTMLElement {
try { try {
const clickLocation = new UIEventSource<Loc>(undefined); const clickLocation = new UIEventSource<Loc>(undefined);
const map = new Minimap( const map = Minimap.createMiniMap(
{ {
location: this._centerLocation, location: this._centerLocation,
background: this.mapBackground, background: this.mapBackground,
@ -198,7 +194,6 @@ export default class LocationInput extends InputElement<Loc> {
}) })
if (this._snapTo !== undefined) { if (this._snapTo !== undefined) {
new ShowDataLayer(this._snapTo, map.leafletMap, State.state.layoutToUse, false, false)
const matchPoint = this._snappedPoint.map(loc => { const matchPoint = this._snappedPoint.map(loc => {
if (loc === undefined) { if (loc === undefined) {
@ -207,16 +202,25 @@ export default class LocationInput extends InputElement<Loc> {
return [{feature: loc}]; return [{feature: loc}];
}) })
if (this._snapTo) { if (this._snapTo) {
let layout = LocationInput.matchLayout if (this._snappedPointTags === undefined) {
if (this._snappedPointTags !== undefined) { // No special tags - we show a default crosshair
layout = State.state.layoutToUse 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
)
} }
} }

View file

@ -4,7 +4,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {SubtleButton} from "../Base/SubtleButton"; import {SubtleButton} from "../Base/SubtleButton";
import Minimap from "../Base/Minimap"; import Minimap from "../Base/Minimap";
import State from "../../State"; import State from "../../State";
import ShowDataLayer from "../ShowDataLayer"; import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import {GeoOperations} from "../../Logic/GeoOperations"; import {GeoOperations} from "../../Logic/GeoOperations";
import {LeafletMouseEvent} from "leaflet"; import {LeafletMouseEvent} from "leaflet";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
@ -13,10 +13,16 @@ import Translations from "../i18n/Translations";
import SplitAction from "../../Logic/Osm/Actions/SplitAction"; import SplitAction from "../../Logic/Osm/Actions/SplitAction";
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
import Title from "../Base/Title"; 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 { 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 * A UI Element used for splitting roads
@ -36,7 +42,7 @@ export default class SplitRoadWizard extends Toggle {
const splitClicked = new UIEventSource<boolean>(false); const splitClicked = new UIEventSource<boolean>(false);
// Minimap on which you can select the points to be splitted // 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;"); miniMap.SetStyle("width: 100%; height: 24rem;");
// Define how a cut is displayed on the map // 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 roadElement = State.state.allElements.ContainingFeatures.get(id)
const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]); const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]);
// Datalayer displaying the road and the cut points (if any) // Datalayer displaying the road and the cut points (if any)
new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true); new ShowDataMultiLayer({
new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false) 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. * Handles a click on the overleaf map.
@ -135,21 +153,4 @@ export default class SplitRoadWizard extends Toggle {
const confirm = new Toggle(mapView, splitToggle, splitClicked); const confirm = new Toggle(mapView, splitToggle, splitClicked);
super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit) 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")
}
} }

View file

@ -1,19 +1,11 @@
/** /**
* The data layer shows all the given geojson elements with the appropriate icon etc * The data layer shows all the given geojson elements with the appropriate icon etc
*/ */
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import * as L from "leaflet" import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import State from "../State"; import FeatureInfoBox from "../Popup/FeatureInfoBox";
import FeatureInfoBox from "./Popup/FeatureInfoBox"; import State from "../../State";
import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
import FeatureSource from "../Logic/FeatureSource/FeatureSource";
export interface ShowDataLayerOptions {
features: FeatureSource,
leafletMap: UIEventSource<L.Map>,
enablePopups?: true | boolean,
zoomToFeatures? : false | boolean,
}
export default class ShowDataLayer { export default class ShowDataLayer {

View file

@ -0,0 +1,9 @@
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
import {UIEventSource} from "../../Logic/UIEventSource";
export interface ShowDataLayerOptions {
features: FeatureSource,
leafletMap: UIEventSource<L.Map>,
enablePopups?: true | boolean,
zoomToFeatures?: false | boolean,
}

View file

@ -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 * 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 { export default class ShowDataMultiLayer {
constructor(options: ShowDataLayerOptions & { layers: UIEventSource<FilteredLayer[]> }) { constructor(options: ShowDataLayerOptions & { layers: UIEventSource<FilteredLayer[]> }) {

View file

@ -5,7 +5,6 @@ import {ImageCarousel} from "./Image/ImageCarousel";
import Combine from "./Base/Combine"; import Combine from "./Base/Combine";
import {FixedUiElement} from "./Base/FixedUiElement"; import {FixedUiElement} from "./Base/FixedUiElement";
import {ImageUploadFlow} from "./Image/ImageUploadFlow"; import {ImageUploadFlow} from "./Image/ImageUploadFlow";
import ShareButton from "./BigComponents/ShareButton"; import ShareButton from "./BigComponents/ShareButton";
import Svg from "../Svg"; import Svg from "../Svg";
import ReviewElement from "./Reviews/ReviewElement"; import ReviewElement from "./Reviews/ReviewElement";
@ -13,7 +12,6 @@ import MangroveReviews from "../Logic/Web/MangroveReviews";
import Translations from "./i18n/Translations"; import Translations from "./i18n/Translations";
import ReviewForm from "./Reviews/ReviewForm"; import ReviewForm from "./Reviews/ReviewForm";
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"; import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization";
import State from "../State"; import State from "../State";
import {ImageSearcher} from "../Logic/Actors/ImageSearcher"; import {ImageSearcher} from "../Logic/Actors/ImageSearcher";
import BaseUIElement from "./BaseUIElement"; import BaseUIElement from "./BaseUIElement";
@ -26,6 +24,9 @@ import BaseLayer from "../Models/BaseLayer";
import LayerConfig from "../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import ImportButton from "./BigComponents/ImportButton"; import ImportButton from "./BigComponents/ImportButton";
import {Tag} from "../Logic/Tags/Tag"; 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 { export interface SpecialVisualization {
funcName: string, funcName: string,
@ -37,14 +38,6 @@ export interface SpecialVisualization {
export default class SpecialVisualizations { export default class SpecialVisualizations {
static constructMiniMap: (options?: {
background?: UIEventSource<BaseLayer>,
location?: UIEventSource<Loc>,
allowMoving?: boolean,
leafletOptions?: any
}) => BaseUIElement;
static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource<any>, layoutToUse: UIEventSource<any>, enablePopups?: boolean, zoomToFeatures?: boolean) => any;
public static specialVisualizations: SpecialVisualization[] = public static specialVisualizations: SpecialVisualization[] =
[ [
{ {
@ -153,7 +146,7 @@ export default class SpecialVisualizations {
lon: Number(properties._lon), lon: Number(properties._lon),
zoom: zoom zoom: zoom
}) })
const minimap = SpecialVisualizations.constructMiniMap( const minimap = Minimap.createMiniMap(
{ {
background: state.backgroundLayer, background: state.backgroundLayer,
location: locationSource, location: locationSource,
@ -169,12 +162,14 @@ export default class SpecialVisualizations {
} }
}) })
SpecialVisualizations.constructShowDataLayer( new ShowDataMultiLayer(
featuresToShow, {
minimap["leafletMap"], leafletMap: minimap["leafletMap"],
State.state.layoutToUse, enablePopups : false,
false, zoomToFeatures: true,
true layers: State.state.filteredLayers,
features: new StaticFeatureSource(featuresToShow, true)
}
) )

View file

@ -245,7 +245,6 @@ export class Utils {
} }
dict.set(k, v()); dict.set(k, v());
return dict.get(k); 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)]] 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 * Return x, y of the tile containing (lat, lon) on the given zoom level
*/ */
@ -422,13 +441,6 @@ export class Utils {
return bestColor ?? hex; 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) { private static tile2long(x, z) {
return (x / Math.pow(2, z) * 360 - 180); return (x / Math.pow(2, z) * 360 - 180);
} }

View file

@ -48,7 +48,7 @@
} }
}, },
"calculatedTags": [ "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)" "_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')) * 1000)"
], ],
"minzoom": 13, "minzoom": 13,

View file

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

View file

@ -39,7 +39,7 @@
"startLat": 0, "startLat": 0,
"startLon": 0, "startLon": 0,
"startZoom": 1, "startZoom": 1,
"widenFactor": 0.05, "widenFactor": 1,
"roamingRenderings": [], "roamingRenderings": [],
"layers": [ "layers": [
"public_bookcase" "public_bookcase"

View file

@ -34,7 +34,7 @@
"defaultBackgroundId": "CartoDB.Voyager", "defaultBackgroundId": "CartoDB.Voyager",
"startLon": 4.351697, "startLon": 4.351697,
"startZoom": 16, "startZoom": 16,
"widenFactor": 0.05, "widenFactor": 2,
"layers": [ "layers": [
"drinking_water" "drinking_water"
], ],

View file

@ -55,6 +55,7 @@
{ {
"#": "Nature reserve overview from cache, points only, z < 13", "#": "Nature reserve overview from cache, points only, z < 13",
"builtin": "nature_reserve", "builtin": "nature_reserve",
"wayHandling": 1,
"override": { "override": {
"source": { "source": {
"osmTags": { "osmTags": {
@ -63,6 +64,7 @@
] ]
}, },
"geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson", "geoJson": "https://pietervdvn.github.io/natuurpunt_cache/natuurpunt_nature_reserve_points.geojson",
"geoJsonZoomLevel": 0,
"isOsmCache": "duplicate" "isOsmCache": "duplicate"
}, },
"minzoom": 1, "minzoom": 1,

View file

@ -152,7 +152,7 @@
] ]
}, },
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson",
"geoJsonZoomLevel": 14, "geoJsonZoomLevel": 11,
"isOsmCache": true "isOsmCache": true
}, },
"title": { "title": {

View file

@ -131,7 +131,7 @@
] ]
}, },
"then": { "then": {
"en": "This object has no house number" "en": "This building has no house number"
} }
} }
] ]

View file

@ -8,31 +8,16 @@ import MoreScreen from "./UI/BigComponents/MoreScreen";
import State from "./State"; import State from "./State";
import Combine from "./UI/Base/Combine"; import Combine from "./UI/Base/Combine";
import Translations from "./UI/i18n/Translations"; 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 ValidatedTextField from "./UI/Input/ValidatedTextField";
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
import Constants from "./Models/Constants"; 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 // 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) ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)
SpecialVisualizations.constructMiniMap = options => new Minimap(options)
SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>,
leafletMap: UIEventSource<L.Map>,
layoutToUse: UIEventSource<LayoutConfig>,
enablePopups = true,
zoomToFeatures = false) => new ShowDataLayer(features, leafletMap, layoutToUse, enablePopups, zoomToFeatures)
let defaultLayout = "" let defaultLayout = ""
// --------------------- Special actions based on the parameters ----------------- // --------------------- Special actions based on the parameters -----------------

View file

@ -159,7 +159,7 @@
"noTagsSelected": "No tags selected", "noTagsSelected": "No tags selected",
"testing": "Testing - changes won't be saved", "testing": "Testing - changes won't be saved",
"customThemeIntro": "<h3>Custom themes</h3>These are previously visited user-generated themes.", "customThemeIntro": "<h3>Custom themes</h3>These are previously visited user-generated themes.",
"aboutMapcomplete": "<h3>About MapComplete</h3><p>With MapComplete you can enrich OpenStreetMap with information on a <b>single theme.</b> Answer a few questions, and within minutes your contributions will be available around the globe! The <b>theme maintainer</b> defines elements, questions and languages for the theme.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete</li><li>The full-screen version offers information about OpenStreetMap</li><li>Viewing works without login, but editing requires an OSM login.</li><li>If you are not logged in, you are asked to log in</li><li>Once you answered a single question, you can add new points to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%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' target='_blank' >OsmCha</a>.</p>", "aboutMapcomplete": "<h3>About MapComplete</h3><p>With MapComplete you can enrich OpenStreetMap with information on a <b>single theme.</b> Answer a few questions, and within minutes your contributions will be available around the globe! The <b>theme maintainer</b> defines elements, questions and languages for the theme.</p><h3>Find out more</h3><p>MapComplete always <b>offers the next step</b> to learn more about OpenStreetMap.<ul><li>When embedded in a website, the iframe links to a full-screen MapComplete</li><li>The full-screen version offers information about OpenStreetMap</li><li>Viewing works without login, but editing requires an OSM login.</li><li>If you are not logged in, you are asked to log in</li><li>Once you answered a single question, you can add new points to the map</li><li>After a while, actual OSM-tags are shown, later linking to the wiki</li></ul></p><br/><p>Did you notice <b>an issue</b>? Do you have a <b>feature request</b>? Want to <b>help translate</b>? Head over to <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>the source code</a> or <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker.</a> </p><p> Want to see <b>your progress</b>? Follow the edit count on <a href='{osmcha_link}' target='_blank' >OsmCha</a>.</p>",
"backgroundMap": "Background map", "backgroundMap": "Background map",
"openTheMap": "Open the map", "openTheMap": "Open the map",
"loginOnlyNeededToEdit": "if you want to edit the map", "loginOnlyNeededToEdit": "if you want to edit the map",

View file

@ -3021,6 +3021,29 @@
} }
}, },
"toilet": { "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", "name": "Toilets",
"presets": { "presets": {
"0": { "0": {

View file

@ -192,7 +192,7 @@
"getStartedNewAccount": " of <a href='https://www.openstreetmap.org/user/new' target='_blank'>maak een nieuwe account aan</a>", "getStartedNewAccount": " of <a href='https://www.openstreetmap.org/user/new' target='_blank'>maak een nieuwe account aan</a>",
"noTagsSelected": "Geen tags geselecteerd", "noTagsSelected": "Geen tags geselecteerd",
"customThemeIntro": "<h3>Onofficiële thema's</h3>De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.", "customThemeIntro": "<h3>Onofficiële thema's</h3>De onderstaande thema's heb je eerder bezocht en zijn gemaakt door andere OpenStreetMappers.",
"aboutMapcomplete": "<h3>Over MapComplete</h3><p>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 <b>maker van het thema</b> bepaalt de elementen, vragen en taalversies voor het thema.</p><h3>Ontdek meer</h3><p>MapComplete <b>biedt altijd de volgende stap</b> naar meer OpenStreetMap:<ul><li>Indien ingebed in een website linkt het iframe naar de volledige MapComplete</li><li>De volledige versie heeft uitleg over OpenStreetMap</li><li>Bekijken kan altijd, maar wijzigen vereist een OSM-account</li><li>Als je niet aangemeld bent, wordt je gevraagd dit te doen</li><li>Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen</li><li>Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki</li></ul></p><p>Merk je <b>een bug</b> of wil je een <b>extra feature</b>? Wil je <b>helpen vertalen</b>? Bezoek dan de <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>broncode</a> en <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker</a>. <p></p>Wil je <b>je vorderingen</b> zien? Volg de edits <a href='https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%222021-01-01%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' target='_blank' >op OsmCha</a>.</p>", "aboutMapcomplete": "<h3>Over MapComplete</h3><p>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 <b>maker van het thema</b> bepaalt de elementen, vragen en taalversies voor het thema.</p><h3>Ontdek meer</h3><p>MapComplete <b>biedt altijd de volgende stap</b> naar meer OpenStreetMap:<ul><li>Indien ingebed in een website linkt het iframe naar de volledige MapComplete</li><li>De volledige versie heeft uitleg over OpenStreetMap</li><li>Bekijken kan altijd, maar wijzigen vereist een OSM-account</li><li>Als je niet aangemeld bent, wordt je gevraagd dit te doen</li><li>Als je minstens één vraag hebt beantwoord, kan je ook elementen toevoegen</li><li>Heb je genoeg changesets, dan verschijnen de OSM-tags, nog later links naar de wiki</li></ul></p><p>Merk je <b>een bug</b> of wil je een <b>extra feature</b>? Wil je <b>helpen vertalen</b>? Bezoek dan de <a href='https://github.com/pietervdvn/MapComplete' target='_blank'>broncode</a> en <a href='https://github.com/pietervdvn/MapComplete/issues' target='_blank'>issue tracker</a>. <p></p>Wil je <b>je vorderingen</b> zien? Volg de edits <a href='{osmcha_link}' target='_blank' >op OsmCha</a>.</p>",
"backgroundMap": "Achtergrondkaart", "backgroundMap": "Achtergrondkaart",
"layerSelection": { "layerSelection": {
"zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien", "zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien",

View file

@ -1351,6 +1351,11 @@
"title": { "title": {
"render": "Known address" "render": "Known address"
} }
},
"2": {
"title": {
"render": "{name}"
}
} }
}, },
"shortDescription": "Help to build an open dataset of UK addresses", "shortDescription": "Help to build an open dataset of UK addresses",

View file

@ -20,10 +20,10 @@
"reset:translations": "ts-node scripts/generateTranslations.ts --ignore-weblate", "reset:translations": "ts-node scripts/generateTranslations.ts --ignore-weblate",
"generate:layouts": "ts-node scripts/generateLayouts.ts", "generate:layouts": "ts-node scripts/generateLayouts.ts",
"generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.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: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: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", "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail",
"query:licenses": "ts-node scripts/generateLicenseInfo.ts --query", "query:licenses": "ts-node scripts/generateLicenseInfo.ts --query",
"generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push", "generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push",

View file

@ -7,7 +7,6 @@ import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
Utils.runningFromConsole = true Utils.runningFromConsole = true
export default class ScriptUtils { export default class ScriptUtils {

View file

@ -2,27 +2,32 @@
* Generates a collection of geojson files based on an overpass query for a given theme * Generates a collection of geojson files based on an overpass query for a given theme
*/ */
import {Utils} from "../Utils"; import {Utils} from "../Utils";
Utils.runningFromConsole = true Utils.runningFromConsole = true
import {Overpass} from "../Logic/Osm/Overpass"; import {Overpass} from "../Logic/Osm/Overpass";
import * as fs from "fs";
import {existsSync, readFileSync, writeFileSync} from "fs"; import {existsSync, readFileSync, writeFileSync} from "fs";
import {TagsFilter} from "../Logic/Tags/TagsFilter"; import {TagsFilter} from "../Logic/Tags/TagsFilter";
import {Or} from "../Logic/Tags/Or"; import {Or} from "../Logic/Tags/Or";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import ExtractRelations from "../Logic/Osm/ExtractRelations"; import RelationsTracker from "../Logic/Osm/RelationsTracker";
import * as OsmToGeoJson from "osmtogeojson"; import * as OsmToGeoJson from "osmtogeojson";
import MetaTagging from "../Logic/MetaTagging"; import MetaTagging from "../Logic/MetaTagging";
import {GeoOperations} from "../Logic/GeoOperations";
import {UIEventSource} from "../Logic/UIEventSource"; import {UIEventSource} from "../Logic/UIEventSource";
import {TileRange} from "../Models/TileRange"; import {TileRange} from "../Models/TileRange";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import ScriptUtils from "./ScriptUtils"; 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() ScriptUtils.fixUtils()
function createOverpassObject(theme: LayoutConfig) { function createOverpassObject(theme: LayoutConfig, relationTracker: RelationsTracker) {
let filters: TagsFilter[] = []; let filters: TagsFilter[] = [];
let extraScripts: string[] = []; let extraScripts: string[] = [];
for (const layer of theme.layers) { 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" throw "Nothing to download! The theme doesn't declare anything to download"
} }
return new Overpass(new Or(filters), extraScripts, new UIEventSource<string>("https://overpass.kumi.systems/api/interpreter"), //https://overpass-api.de/api/interpreter"), return new Overpass(new Or(filters), extraScripts, new UIEventSource<string>("https://overpass.kumi.systems/api/interpreter"), //https://overpass-api.de/api/interpreter"),
new UIEventSource<number>(60)); new UIEventSource<number>(60), relationTracker);
} }
function rawJsonName(targetDir: string, x: number, y: number, z: number): string { 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++; downloaded++;
const filename = rawJsonName(targetdir, x, y, r.zoomlevel) const filename = rawJsonName(targetdir, x, y, r.zoomlevel)
if (existsSync(filename)) { if (existsSync(filename)) {
console.log("Already exists: ", filename) console.log("Already exists (not downloading again): ", filename)
skipped++ skipped++
continue; continue;
} }
@ -145,14 +150,16 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ {
return allFeatures; 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; let processed = 0;
const layerIndex = theme.LayerIndex();
for (let x = r.xstart; x <= r.xend; x++) { for (let x = r.xstart; x <= r.xend; x++) {
for (let y = r.ystart; y <= r.yend; y++) { for (let y = r.ystart; y <= r.yend; y++) {
processed++; processed++;
const filename = rawJsonName(targetdir, x, y, r.zoomlevel) 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)) { if (!existsSync(filename)) {
console.error("Not found - and not downloaded. Run this script again!: " + filename) console.error("Not found - and not downloaded. Run this script again!: " + filename)
continue; 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 // Create and save the geojson file - which is the main chunk of the data
const geojson = OsmToGeoJson.default(rawOsm); 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; * Load all the tiles into memory from disk
const generated = {} // layer --> x --> y[] */
for (let x = r.xstart; x <= r.xend; x++) { function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string) {
for (let y = r.ystart; y <= r.yend; y++) {
const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8")
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";
}) function handleLayer(source: FeatureSourceForLayer) {
const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z); const layer = source.layer.layerDef;
ScriptUtils.erasableLog(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")") const layerId = layer.id
if (geojson.features.length == 0) { if (layer.source.isOsmCacheLayer !== true) {
continue; 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) { const createdTiles = []
generated[layer.id] = {} // 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) { if (tile.features.data.length === 0) {
generated[layer.id][x] = [] 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, " "))
} }
} })
}
// All the tiles are written at this point
for (const layer of theme.layers) { // Only thing left to do is to create the index
const id = layer.id const path = targetdir + "_" + layerId + "_overview.json"
const loaded = generated[id] const perX = {}
if (loaded === undefined) { createdTiles.map(i => Utils.tile_from_index(i)).forEach(([z, x, y]) => {
console.log("No features loaded for layer ", id) const key = "" + x
continue; if (perX[key] === undefined) {
} perX[key] = []
writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded)) }
perX[key].push(y)
})
writeFileSync(path, JSON.stringify(perX))
} }
new PerLayerFeatureSourceSplitter(
new UIEventSource<FilteredLayer[]>(theme.layers.map(l => ({
layerDef: l,
isDisplayed: new UIEventSource<boolean>(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<string>()
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[]) { 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); console.error("The theme " + theme + " was not found; try one of ", keys);
return return
} }
const relationTracker = new RelationsTracker()
const overpass = createOverpassObject(theme) const overpass = createOverpassObject(theme, relationTracker)
let failed = 0; let failed = 0;
do { do {
@ -348,21 +300,13 @@ async function main(args: string[]) {
} while (failed > 0) } while (failed > 0)
const extraFeatures = await downloadExtraData(theme); const extraFeatures = await downloadExtraData(theme);
postProcess(targetdir, tileRange, theme, extraFeatures) const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures)
splitPerLayer(targetdir, tileRange, theme) 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] let args = [...process.argv]
args.splice(0, 2) args.splice(0, 2)
main(args); main(args);
console.log("All done!")