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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ import FeatureSource from "../FeatureSource/FeatureSource";
import {OsmObject} from "../Osm/OsmObject";
import Loc from "../../Models/Loc";
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
import OsmApiFeatureSource from "../FeatureSource/OsmApiFeatureSource";
import OsmApiFeatureSource from "../FeatureSource/Sources/OsmApiFeatureSource";
/**
* Makes sure the hash shows the selected element and vice-versa.

View file

@ -1,21 +1,49 @@
/// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
import FeatureSource from "./FeatureSource/FeatureSource";
import {UIEventSource} from "./UIEventSource";
import FeaturePipeline from "./FeatureSource/FeaturePipeline";
import Loc from "../Models/Loc";
import State from "../State";
import {BBox} from "./GeoOperations";
export default class ContributorCount {
public readonly Contributors: UIEventSource<Map<string, number>>;
public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>());
private readonly state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> };
constructor(featureSource: FeatureSource) {
this.Contributors = featureSource.features.map(features => {
const hist = new Map<string, number>();
for (const feature of features) {
const contributor = feature.feature.properties["_last_edit:contributor"]
constructor(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc> }) {
this.state = state;
const self = this;
state.currentBounds.map(bbox => {
self.update(bbox)
})
state.featurePipeline.runningQuery.addCallbackAndRun(
_ => self.update(state.currentBounds.data)
)
}
private lastUpdate: Date = undefined;
private update(bbox: BBox) {
if(bbox === undefined){
return;
}
const now = new Date();
if (this.lastUpdate !== undefined && ((now.getTime() - this.lastUpdate.getTime()) < 1000 * 60)) {
return;
}
console.log("Calculating contributors")
const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox)
const hist = new Map<string, number>();
for (const list of featuresList) {
for (const feature of list) {
const contributor = feature.properties["_last_edit:contributor"]
const count = hist.get(contributor) ?? 0;
hist.set(contributor, count + 1)
}
return hist;
})
}
this.Contributors.setData(hist)
}
}

View file

@ -1,14 +1,24 @@
import {GeoOperations} from "./GeoOperations";
import {BBox, GeoOperations} from "./GeoOperations";
import Combine from "../UI/Base/Combine";
import {Relation} from "./Osm/ExtractRelations";
import RelationsTracker from "./Osm/RelationsTracker";
import State from "../State";
import {Utils} from "../Utils";
import BaseUIElement from "../UI/BaseUIElement";
import List from "../UI/Base/List";
import Title from "../UI/Base/Title";
import {UIEventSourceTools} from "./UIEventSource";
import AspectedRouting from "./Osm/aspectedRouting";
export interface ExtraFuncParams {
/**
* Gets all the features from the given layer within the given BBOX.
* Note that more features then requested can be given back.
* Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...]
*/
getFeaturesWithin: (layerId: string, bbox: BBox) => any[][],
memberships: RelationsTracker
}
export class ExtraFunction {
@ -55,15 +65,20 @@ export class ExtraFunction {
(params, feat) => {
return (...layerIds: string[]) => {
const result = []
const bbox = BBox.get(feat)
for (const layerId of layerIds) {
const otherLayer = params.featuresPerLayer.get(layerId);
if (otherLayer === undefined) {
const otherLayers = params.getFeaturesWithin(layerId, bbox)
if (otherLayers === undefined) {
continue;
}
if (otherLayer.length === 0) {
if (otherLayers.length === 0) {
continue;
}
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
for (const otherLayer of otherLayers) {
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
}
}
return result;
}
@ -77,6 +92,9 @@ export class ExtraFunction {
},
(featuresPerLayer, feature) => {
return (arg0, lat) => {
if (arg0 === undefined) {
return undefined;
}
if (typeof arg0 === "number") {
// Feature._lon and ._lat is conveniently place by one of the other metatags
return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]);
@ -103,7 +121,7 @@ export class ExtraFunction {
args: ["list of features"]
},
(params, feature) => {
return (features) => ExtraFunction.GetClosestNFeatures(params, feature, features)[0].feat
return (features) => ExtraFunction.GetClosestNFeatures(params, feature, features)?.[0]?.feat
}
)
@ -113,12 +131,13 @@ export class ExtraFunction {
doc: "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature. In the case of ways/polygons, only the centerpoint is considered. " +
"Returns a list of `{feat: geojson, distance:number}` the empty list if nothing is found (or not yet laoded)\n\n" +
"If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)",
args: ["list of features", "amount of features", "unique tag key (optional)"]
args: ["list of features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"]
},
(params, feature) => {
return (features, amount, uniqueTag) => ExtraFunction.GetClosestNFeatures(params, feature, features, {
return (features, amount, uniqueTag, maxDistanceInMeters) => ExtraFunction.GetClosestNFeatures(params, feature, features, {
maxFeatures: Number(amount),
uniqueTag: uniqueTag
uniqueTag: uniqueTag,
maxDistance: Number(maxDistanceInMeters)
})
}
)
@ -131,8 +150,10 @@ export class ExtraFunction {
"For example: `_part_of_walking_routes=feat.memberships().map(r => r.relation.tags.name).join(';')`",
args: []
},
(params, _) => {
return () => params.relations ?? [];
(params, feat) => {
return () =>
params.memberships.knownRelations.data.get(feat.properties.id) ?? []
}
)
private static readonly AspectedRouting = new ExtraFunction(
@ -165,19 +186,19 @@ export class ExtraFunction {
private readonly _name: string;
private readonly _args: string[];
private readonly _doc: string;
private readonly _f: (params: { featuresPerLayer: Map<string, any[]>, relations: { role: string, relation: Relation }[] }, feat: any) => any;
private readonly _f: (params: ExtraFuncParams, feat: any) => any;
constructor(options: { name: string, doc: string, args: string[] },
f: ((params: { featuresPerLayer: Map<string, any[]>, relations: { role: string, relation: Relation }[] }, feat: any) => any)) {
f: ((params: ExtraFuncParams, feat: any) => any)) {
this._name = options.name;
this._doc = options.doc;
this._args = options.args;
this._f = f;
}
public static FullPatchFeature(featuresPerLayer: Map<string, any[]>, relations: { role: string, relation: Relation }[], feature) {
public static FullPatchFeature(params: ExtraFuncParams, feature) {
for (const func of ExtraFunction.allFuncs) {
func.PatchFeature(featuresPerLayer, relations, feature);
func.PatchFeature(params, feature);
}
}
@ -198,121 +219,132 @@ export class ExtraFunction {
}
/**
* Gets the closes N features, sorted by ascending distance
* Gets the closes N features, sorted by ascending distance.
*
* @param params: The link to mapcomplete state
* @param feature: The central feature under consideration
* @param features: The other features
* @param options: maxFeatures: The maximum amount of features to be returned. Default: 1; uniqueTag: returned features are not allowed to have the same value for this key; maxDistance: stop searching if it is too far away (in meter). Default: 500m
* @constructor
* @private
*/
private static GetClosestNFeatures(params, feature, features, options?: { maxFeatures?: number, uniqueTag?: string | undefined }): { feat: any, distance: number }[] {
private static GetClosestNFeatures(params: ExtraFuncParams,
feature: any,
features: string | any[],
options?: { maxFeatures?: number, uniqueTag?: string | undefined, maxDistance?: number }): { feat: any, distance: number }[] {
const maxFeatures = options?.maxFeatures ?? 1
const uniqueTag : string | undefined = options?.uniqueTag
const maxDistance = options?.maxDistance ?? 500
const uniqueTag: string | undefined = options?.uniqueTag
if (typeof features === "string") {
const name = features
features = params.featuresPerLayer.get(features)
if (features === undefined) {
var keys = Utils.NoNull(Array.from(params.featuresPerLayer.keys()));
if (keys.length > 0) {
throw `No features defined for ${name}. Defined layers are ${keys.join(", ")}`;
} else {
// This is the first pass over an external dataset
// Other data probably still has to load!
return undefined;
}
}
const bbox = GeoOperations.bbox(GeoOperations.buffer(GeoOperations.bbox(feature), maxDistance))
features = params.getFeaturesWithin(name, new BBox(bbox.geometry.coordinates))
}else{
features = [features]
}
if (features === undefined) {
return;
}
let closestFeatures: { feat: any, distance: number }[] = [];
for (const otherFeature of features) {
if (otherFeature == feature || otherFeature.id == feature.id) {
continue; // We ignore self
}
let distance = undefined;
if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) {
distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]);
} else {
distance = GeoOperations.distanceBetween(
GeoOperations.centerpointCoordinates(otherFeature),
[feature._lon, feature._lat]
)
}
if (distance === undefined) {
throw "Undefined distance!"
}
for(const featureList of features) {
for (const otherFeature of featureList) {
if (otherFeature == feature || otherFeature.id == feature.id) {
continue; // We ignore self
}
let distance = undefined;
if (otherFeature._lon !== undefined && otherFeature._lat !== undefined) {
distance = GeoOperations.distanceBetween([otherFeature._lon, otherFeature._lat], [feature._lon, feature._lat]);
} else {
distance = GeoOperations.distanceBetween(
GeoOperations.centerpointCoordinates(otherFeature),
[feature._lon, feature._lat]
)
}
if (distance === undefined) {
throw "Undefined distance!"
}
if (distance > maxDistance) {
continue
}
if (closestFeatures.length === 0) {
closestFeatures.push({
feat: otherFeature,
distance: distance
})
continue;
}
if (closestFeatures.length === 0) {
closestFeatures.push({
feat: otherFeature,
distance: distance
})
continue;
}
if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) {
// The last feature of the list (and thus the furthest away is still closer
// No use for checking, as we already have plenty of features!
continue
}
if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) {
// The last feature of the list (and thus the furthest away is still closer
// No use for checking, as we already have plenty of features!
continue
}
let targetIndex = closestFeatures.length
for (let i = 0; i < closestFeatures.length; i++) {
const closestFeature = closestFeatures[i];
let targetIndex = closestFeatures.length
for (let i = 0; i < closestFeatures.length; i++) {
const closestFeature = closestFeatures[i];
if (uniqueTag !== undefined) {
const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined &&
closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag]
if (uniqueTagsMatch) {
targetIndex = -1
if (closestFeature.distance > distance) {
// This is a very special situation:
// We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads')
// AT this point, we have found a closer segment with the same, identical tag
// so we replace directly
closestFeatures[i] = {feat: otherFeature, distance: distance}
if (uniqueTag !== undefined) {
const uniqueTagsMatch = otherFeature.properties[uniqueTag] !== undefined &&
closestFeature.feat.properties[uniqueTag] === otherFeature.properties[uniqueTag]
if (uniqueTagsMatch) {
targetIndex = -1
if (closestFeature.distance > distance) {
// This is a very special situation:
// We want to see the tag `uniquetag=some_value` only once in the entire list (e.g. to prevent road segements of identical names to fill up the list of 'names of nearby roads')
// AT this point, we have found a closer segment with the same, identical tag
// so we replace directly
closestFeatures[i] = {feat: otherFeature, distance: distance}
}
break;
}
}
if (closestFeature.distance > distance) {
targetIndex = i
if (uniqueTag !== undefined) {
const uniqueValue = otherFeature.properties[uniqueTag]
// We might still have some other values later one with the same uniquetag that have to be cleaned
for (let j = i; j < closestFeatures.length; j++) {
if (closestFeatures[j].feat.properties[uniqueTag] === uniqueValue) {
closestFeatures.splice(j, 1)
}
}
}
break;
}
}
if (closestFeature.distance > distance) {
targetIndex = i
if (targetIndex == -1) {
continue; // value is already swapped by the unique tag
}
if (uniqueTag !== undefined) {
const uniqueValue = otherFeature.properties[uniqueTag]
// We might still have some other values later one with the same uniquetag that have to be cleaned
for (let j = i; j < closestFeatures.length; j++) {
if(closestFeatures[j].feat.properties[uniqueTag] === uniqueValue){
closestFeatures.splice(j, 1)
}
}
if (targetIndex < maxFeatures) {
// insert and drop one
closestFeatures.splice(targetIndex, 0, {
feat: otherFeature,
distance: distance
})
if (closestFeatures.length >= maxFeatures) {
closestFeatures.splice(maxFeatures, 1)
}
} else {
// Overwrite the last element
closestFeatures[targetIndex] = {
feat: otherFeature,
distance: distance
}
break;
}
}
if (targetIndex == -1) {
continue; // value is already swapped by the unique tag
}
if (targetIndex < maxFeatures) {
// insert and drop one
closestFeatures.splice(targetIndex, 0, {
feat: otherFeature,
distance: distance
})
if (closestFeatures.length >= maxFeatures) {
closestFeatures.splice(maxFeatures, 1)
}
} else {
// Overwrite the last element
closestFeatures[targetIndex] = {
feat: otherFeature,
distance: distance
}
}
}
return closestFeatures;
}
public PatchFeature(featuresPerLayer: Map<string, any[]>, relations: { role: string, relation: Relation }[], feature: any) {
feature[this._name] = this._f({featuresPerLayer: featuresPerLayer, relations: relations}, feature)
public PatchFeature(params: ExtraFuncParams, feature: any) {
feature[this._name] = this._f(params, feature)
}
}

View file

@ -1,11 +1,11 @@
import {FeatureSourceForLayer} from "./FeatureSource";
import {Utils} from "../../Utils";
/***
* Saves all the features that are passed in to localstorage, so they can be retrieved on the next run
*
* Technically, more an Actor then a featuresource, but it fits more neatly this ay
*/
import {FeatureSourceForLayer} from "../FeatureSource";
import {Utils} from "../../../Utils";
export default class LocalStorageSaverActor {
public static readonly storageKey: string = "cached-features";
@ -21,7 +21,6 @@ export default class LocalStorageSaverActor {
try {
localStorage.setItem(key, JSON.stringify(features));
console.log("Saved ", features.length, "elements to", key)
localStorage.setItem(key + "-time", JSON.stringify(now))
} catch (e) {
console.warn("Could not save the features to local storage:", e)

View file

@ -1,6 +1,6 @@
import FeatureSource from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import State from "../../State";
import FeatureSource from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
import State from "../../../State";
export default class RegisteringAllFromFeatureSourceActor {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;

View file

@ -1,10 +1,35 @@
import FeatureSource from "./FeatureSource";
import FeatureSource, {IndexedFeatureSource} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import {Changes} from "../Osm/Changes";
import {ChangeDescription} from "../Osm/Actions/ChangeDescription";
import {Utils} from "../../Utils";
import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject";
/**
* A feature source containing exclusively new elements
*/
export class NewGeometryChangeApplicatorFeatureSource implements FeatureSource{
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name: string = "newFeatures";
constructor(changes: Changes) {
const seenChanges = new Set<ChangeDescription>();
changes.pendingChanges.addCallbackAndRunD(changes => {
for (const change of changes) {
if(seenChanges.has(change)){
continue
}
seenChanges.add(change)
if(change.id < 0){
// This is a new object!
}
}
})
}
}
/**
* Applies changes from 'Changes' onto a featureSource
@ -12,10 +37,18 @@ import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject";
export default class ChangeApplicator implements FeatureSource {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name: string;
private readonly source: IndexedFeatureSource;
private readonly changes: Changes;
private readonly mode?: {
generateNewGeometries: boolean
};
constructor(source: FeatureSource, changes: Changes, mode?: {
constructor(source: IndexedFeatureSource, changes: Changes, mode?: {
generateNewGeometries: boolean
}) {
this.source = source;
this.changes = changes;
this.mode = mode;
this.name = "ChangesApplied(" + source.name + ")"
this.features = source.features
@ -26,7 +59,7 @@ export default class ChangeApplicator implements FeatureSource {
if (runningUpdate) {
return; // No need to ping again
}
ChangeApplicator.ApplyChanges(features, changes.pendingChanges.data, mode)
self.ApplyChanges()
seenChanges.clear()
})
@ -34,19 +67,20 @@ export default class ChangeApplicator implements FeatureSource {
runningUpdate = true;
changes = changes.filter(ch => !seenChanges.has(ch))
changes.forEach(c => seenChanges.add(c))
ChangeApplicator.ApplyChanges(self.features.data, changes, mode)
self.ApplyChanges()
source.features.ping()
runningUpdate = false;
})
}
/**
* Returns true if the geometry is changed and the source should be pinged
*/
private static ApplyChanges(features: { feature: any; freshness: Date }[], cs: ChangeDescription[], mode: { generateNewGeometries: boolean }): boolean {
private ApplyChanges(): boolean {
const cs = this.changes.pendingChanges.data
const features = this.source.features.data
const loadedIds = this.source.containedIds
if (cs.length === 0 || features === undefined) {
return;
}
@ -56,12 +90,18 @@ export default class ChangeApplicator implements FeatureSource {
const changesPerId: Map<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>()
for (const c of cs) {
const id = c.type + "/" + c.id
if (!loadedIds.has(id)) {
continue
}
if (!changesPerId.has(id)) {
changesPerId.set(id, [])
}
changesPerId.get(id).push(c)
}
if (changesPerId.size === 0) {
// The current feature source set doesn't contain any changed feature, so we can safely skip
return;
}
const now = new Date()
@ -77,7 +117,7 @@ export default class ChangeApplicator implements FeatureSource {
// First, create the new features - they have a negative ID
// We don't set the properties yet though
if (mode?.generateNewGeometries) {
if (this.mode?.generateNewGeometries) {
changesPerId.forEach(cs => {
cs
.forEach(change => {

View file

@ -1,95 +1,191 @@
import FilteringFeatureSource from "../FeatureSource/FilteringFeatureSource";
import FeatureSourceMerger from "../FeatureSource/FeatureSourceMerger";
import RememberingSource from "../FeatureSource/RememberingSource";
import WayHandlingApplyingFeatureSource from "../FeatureSource/WayHandlingApplyingFeatureSource";
import FeatureDuplicatorPerLayer from "../FeatureSource/FeatureDuplicatorPerLayer";
import FeatureSource from "../FeatureSource/FeatureSource";
import {UIEventSource} from "../UIEventSource";
import LocalStorageSaver from "./LocalStorageSaver";
import LocalStorageSource from "./LocalStorageSource";
import Loc from "../../Models/Loc";
import GeoJsonSource from "./GeoJsonSource";
import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource";
import RegisteringFeatureSource from "./RegisteringFeatureSource";
import FilteredLayer from "../../Models/FilteredLayer";
import {Changes} from "../Osm/Changes";
import ChangeApplicator from "./ChangeApplicator";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import FilteringFeatureSource from "./Sources/FilteringFeatureSource";
import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter";
import FeatureSource, {FeatureSourceForLayer, FeatureSourceState, Tiled} from "./FeatureSource";
import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource";
import {UIEventSource} from "../UIEventSource";
import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy";
import FilteredLayer from "../../Models/FilteredLayer";
import MetaTagging from "../MetaTagging";
import RememberingSource from "./Sources/RememberingSource";
import OverpassFeatureSource from "../Actors/OverpassFeatureSource";
import {Changes} from "../Osm/Changes";
import GeoJsonSource from "./Sources/GeoJsonSource";
import Loc from "../../Models/Loc";
import WayHandlingApplyingFeatureSource from "./Sources/WayHandlingApplyingFeatureSource";
import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor";
import {Utils} from "../../Utils";
import TiledFromLocalStorageSource from "./TiledFeatureSource/TiledFromLocalStorageSource";
import LocalStorageSaverActor from "./Actors/LocalStorageSaverActor";
import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource";
import {BBox} from "../GeoOperations";
import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger";
import RelationsTracker from "../Osm/RelationsTracker";
export default class FeaturePipeline implements FeatureSource {
public features: UIEventSource<{ feature: any; freshness: Date }[]>;
export default class FeaturePipeline implements FeatureSourceState {
public readonly name = "FeaturePipeline"
public readonly sufficientlyZoomed: UIEventSource<boolean>;
public readonly runningQuery: UIEventSource<boolean>;
public readonly timeout: UIEventSource<number>;
public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false)
constructor(flayers: UIEventSource<FilteredLayer[]>,
changes: Changes,
updater: FeatureSource,
fromOsmApi: FeatureSource,
layout: UIEventSource<LayoutConfig>,
locationControl: UIEventSource<Loc>,
selectedElement: UIEventSource<any>) {
private readonly overpassUpdater: OverpassFeatureSource
private readonly relationTracker: RelationsTracker
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>;
constructor(
handleFeatureSource: (source: FeatureSourceForLayer) => void,
state: {
osmApiFeatureSource: FeatureSource,
filteredLayers: UIEventSource<FilteredLayer[]>,
locationControl: UIEventSource<Loc>,
selectedElement: UIEventSource<any>,
changes: Changes,
layoutToUse: UIEventSource<LayoutConfig>,
leafletMap: any,
readonly overpassUrl: UIEventSource<string>;
readonly overpassTimeout: UIEventSource<number>;
readonly overpassMaxZoom: UIEventSource<number>;
}) {
const allLoadedFeatures = new UIEventSource<{ feature: any; freshness: Date }[]>([])
const self = this
const updater = new OverpassFeatureSource(state);
this.overpassUpdater = updater;
this.sufficientlyZoomed = updater.sufficientlyZoomed
this.runningQuery = updater.runningQuery
this.timeout = updater.timeout
this.relationTracker = updater.relationsTracker
// Register everything in the state' 'AllElements'
new RegisteringAllFromFeatureSourceActor(updater)
// first we metatag, then we save to get the metatags into storage too
// Note that we need to register before we do metatagging (as it expects the event sources)
const perLayerHierarchy = new Map<string, TileHierarchyMerger>()
this.perLayerHierarchy = perLayerHierarchy
// AT last, the metaTagging also needs to be run _after_ the duplicatorPerLayer
const amendedOverpassSource =
new RememberingSource(
new LocalStorageSaver(
new MetaTaggingFeatureSource(allLoadedFeatures,
new FeatureDuplicatorPerLayer(flayers,
new RegisteringFeatureSource(
new ChangeApplicator(
updater, changes
))
)), layout));
const patchedHandleFeatureSource = function (src: FeatureSourceForLayer) {
// This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
const srcFiltered =
new FilteringFeatureSource(state,
new WayHandlingApplyingFeatureSource(
src,
)
)
handleFeatureSource(srcFiltered)
self.somethingLoaded.setData(true)
};
const geojsonSources: FeatureSource [] = GeoJsonSource
.ConstructMultiSource(flayers.data, locationControl)
.map(geojsonSource => {
let source = new RegisteringFeatureSource(
new FeatureDuplicatorPerLayer(flayers,
new ChangeApplicator(geojsonSource, changes)));
if (!geojsonSource.isOsmCache) {
source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features);
}
return source
});
function addToHierarchy(src: FeatureSource & Tiled, layerId: string) {
perLayerHierarchy.get(layerId).registerTile(src)
}
const amendedLocalStorageSource =
new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new ChangeApplicator(new LocalStorageSource(layout), changes))
));
for (const filteredLayer of state.filteredLayers.data) {
const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => patchedHandleFeatureSource(tile))
const id = filteredLayer.layerDef.id
perLayerHierarchy.set(id, hierarchy)
const source = filteredLayer.layerDef.source
const amendedOsmApiSource = new RememberingSource(
new MetaTaggingFeatureSource(allLoadedFeatures,
new FeatureDuplicatorPerLayer(flayers,
new RegisteringFeatureSource(new ChangeApplicator(fromOsmApi, changes,
{
// We lump in the new points here
generateNewGeometries: true
if (source.geojsonSource === undefined) {
// This is an OSM layer
// We load the cached values and register them
// Getting data from upstream happens a bit lower
new TiledFromLocalStorageSource(filteredLayer,
(src) => {
new RegisteringAllFromFeatureSourceActor(src)
hierarchy.registerTile(src);
}, state)
continue
}
if (source.geojsonZoomLevel === undefined) {
// This is a 'load everything at once' geojson layer
// We split them up into tiles
const src = new GeoJsonSource(filteredLayer)
TiledFeatureSource.createHierarchy(src, {
layer: src.layer,
registerTile: (tile) => {
new RegisteringAllFromFeatureSourceActor(tile)
addToHierarchy(tile, id)
}
})
} else {
new DynamicGeoJsonTileSource(
filteredLayer,
src => TiledFeatureSource.createHierarchy(src, {
layer: src.layer,
registerTile: (tile) => {
new RegisteringAllFromFeatureSourceActor(tile)
addToHierarchy(tile, id)
}
)))));
}),
state
)
}
const merged =
new FeatureSourceMerger([
amendedOverpassSource,
amendedOsmApiSource,
amendedLocalStorageSource,
...geojsonSources
]);
}
merged.features.syncWith(allLoadedFeatures)
// Actually load data from the overpass source
new PerLayerFeatureSourceSplitter(state.filteredLayers,
(source) => TiledFeatureSource.createHierarchy(source, {
layer: source.layer,
registerTile: (tile) => {
// We save the tile data for the given layer to local storage
const [z, x, y] = Utils.tile_from_index(tile.tileIndex)
new LocalStorageSaverActor(tile, x, y, z)
addToHierarchy(tile, source.layer.layerDef.id);
}
}), new RememberingSource(updater))
// Whenever fresh data comes in, we need to update the metatagging
updater.features.addCallback(_ => {
self.updateAllMetaTagging()
})
this.features = new WayHandlingApplyingFeatureSource(flayers,
new FilteringFeatureSource(
flayers,
locationControl,
selectedElement,
merged
)).features;
}
private updateAllMetaTagging() {
console.log("Updating the meta tagging")
const self = this;
this.perLayerHierarchy.forEach(hierarchy => {
hierarchy.loadedTiles.forEach(src => {
MetaTagging.addMetatags(
src.features.data,
{
memberships: this.relationTracker,
getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox)
},
src.layer.layerDef
)
})
})
}
public GetAllFeaturesWithin(bbox: BBox): any[][]{
const self = this
const tiles = []
Array.from(this.perLayerHierarchy.keys())
.forEach(key => tiles.push(...self.GetFeaturesWithin(key, bbox)))
return tiles;
}
public GetFeaturesWithin(layerId: string, bbox: BBox): any[][]{
const requestedHierarchy = this.perLayerHierarchy.get(layerId)
if (requestedHierarchy === undefined) {
return undefined;
}
return TileHierarchyTools.getTiles(requestedHierarchy, bbox)
.filter(featureSource => featureSource.features?.data !== undefined)
.map(featureSource => featureSource.features.data.map(fs => fs.feature))
}
public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void){
Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => {
TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
})
}
public ForceRefresh() {
this.overpassUpdater.ForceRefresh()
}
}

View file

@ -1,5 +1,7 @@
import {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils";
import FilteredLayer from "../../Models/FilteredLayer";
import {BBox} from "../GeoOperations";
export default interface FeatureSource {
features: UIEventSource<{ feature: any, freshness: Date }[]>;
@ -9,38 +11,30 @@ export default interface FeatureSource {
name: string;
}
export class FeatureSourceUtils {
export interface Tiled {
tileIndex: number,
bbox: BBox
}
/**
* Exports given featurePipeline as a geojson FeatureLists (downloads as a json)
* @param featurePipeline The FeaturePipeline you want to export
* @param options The options object
* @param options.metadata True if you want to include the MapComplete metadata, false otherwise
*/
public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) {
let defaults = {
metadata: false,
}
options = Utils.setDefaults(options, defaults);
/**
* A feature source which only contains features for the defined layer
*/
export interface FeatureSourceForLayer extends FeatureSource{
readonly layer: FilteredLayer
}
// Select all features, ignore the freshness and other data
let featureList: any[] = featurePipeline.features.data.map((feature) =>
JSON.parse(JSON.stringify((feature.feature)))); // Make a deep copy!
/**
* A feature source which is aware of the indexes it contains
*/
export interface IndexedFeatureSource extends FeatureSource {
readonly containedIds: UIEventSource<Set<string>>
}
if (!options.metadata) {
for (let i = 0; i < featureList.length; i++) {
let feature = featureList[i];
for (let property in feature.properties) {
if (property[0] == "_" && property !== "_lat" && property !== "_lon") {
delete featureList[i]["properties"][property];
}
}
}
}
return {type: "FeatureCollection", features: featureList}
}
}
/**
* A feature source which has some extra data about it's state
*/
export interface FeatureSourceState {
readonly sufficientlyZoomed: UIEventSource<boolean>;
readonly runningQuery: UIEventSource<boolean>;
readonly timeout: UIEventSource<number>;
}

View file

@ -1,8 +1,7 @@
import FeatureSource from "./FeatureSource";
import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import FilteredLayer from "../../Models/FilteredLayer";
import OverpassFeatureSource from "../Actors/OverpassFeatureSource";
import SimpleFeatureSource from "./SimpleFeatureSource";
import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
/**
@ -13,17 +12,17 @@ import SimpleFeatureSource from "./SimpleFeatureSource";
export default class PerLayerFeatureSourceSplitter {
constructor(layers: UIEventSource<FilteredLayer[]>,
handleLayerData: (source: FeatureSource) => void,
upstream: OverpassFeatureSource) {
handleLayerData: (source: FeatureSourceForLayer) => void,
upstream: FeatureSource) {
const knownLayers = new Map<string, FeatureSource>()
const knownLayers = new Map<string, FeatureSourceForLayer>()
function update() {
const features = upstream.features.data;
if (features === undefined) {
return;
}
if(layers.data === undefined){
if (layers.data === undefined) {
return;
}
@ -69,19 +68,16 @@ export default class PerLayerFeatureSourceSplitter {
if (featureSource === undefined) {
// Not yet initialized - now is a good time
featureSource = new SimpleFeatureSource(layer)
featureSource.features.setData(features)
knownLayers.set(id, featureSource)
handleLayerData(featureSource)
} else {
featureSource.features.setData(features)
}
featureSource.features.setData(features)
}
upstream.features.addCallbackAndRunD(_ => update())
layers.addCallbackAndRunD(_ => update())
}
layers.addCallbackAndRunD(_ => update())
layers.addCallback(_ => update())
upstream.features.addCallbackAndRunD(_ => update())
}
}

View file

@ -1,22 +1,28 @@
import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import FilteredLayer from "../../Models/FilteredLayer";
/**
* Merges features from different featureSources for a single layer
* Uses the freshest feature available in the case multiple sources offer data with the same identifier
*/
export default class FeatureSourceMerger implements FeatureSourceForLayer {
import {UIEventSource} from "../../UIEventSource";
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import FilteredLayer from "../../../Models/FilteredLayer";
import {BBox} from "../../GeoOperations";
import {Utils} from "../../../Utils";
export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled {
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name;
public readonly layer: FilteredLayer
private readonly _sources: UIEventSource<FeatureSource[]>;
public readonly tileIndex: number;
public readonly bbox: BBox;
constructor(layer: FilteredLayer ,sources: UIEventSource<FeatureSource[]>) {
constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
this.tileIndex = tileIndex;
this.bbox = bbox;
this._sources = sources;
this.layer = layer;
this.name = "SourceMerger"
this.name = "FeatureSourceMerger("+layer.layerDef.id+", "+Utils.tile_from_index(tileIndex).join(",")+")"
const self = this;
const handledSources = new Set<FeatureSource>();

View file

@ -1,13 +1,13 @@
import {FeatureSourceForLayer} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import Hash from "../Web/Hash";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import FilteredLayer from "../../Models/FilteredLayer";
import {UIEventSource} from "../../UIEventSource";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer} from "../FeatureSource";
import Hash from "../../Web/Hash";
export default class FilteringFeatureSource implements FeatureSourceForLayer {
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
new UIEventSource<{ feature: any; freshness: Date }[]>([]);
public readonly name = "FilteringFeatureSource";
public readonly name;
public readonly layer: FilteredLayer;
constructor(
@ -18,6 +18,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer {
upstream: FeatureSourceForLayer
) {
const self = this;
this.name = "FilteringFeatureSource("+upstream.name+")"
this.layer = upstream.layer;
const layer = upstream.layer;

View file

@ -1,14 +1,14 @@
import {FeatureSourceForLayer} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils";
import FilteredLayer from "../../Models/FilteredLayer";
import {control} from "leaflet";
/**
* Fetches a geojson file somewhere and passes it along
*/
export default class GeoJsonSource implements FeatureSourceForLayer {
import {UIEventSource} from "../../UIEventSource";
import FilteredLayer from "../../../Models/FilteredLayer";
import {Utils} from "../../../Utils";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {BBox} from "../../GeoOperations";
export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name;
@ -17,6 +17,8 @@ export default class GeoJsonSource implements FeatureSourceForLayer {
private readonly seenids: Set<string> = new Set<string>()
public readonly layer: FilteredLayer;
public readonly tileIndex
public readonly bbox;
public constructor(flayer: FilteredLayer,
zxy?: [number, number, number]) {
@ -28,10 +30,16 @@ export default class GeoJsonSource implements FeatureSourceForLayer {
this.layer = flayer;
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
if (zxy !== undefined) {
const [z, x, y] = zxy;
url = url
.replace('{z}', "" + zxy[0])
.replace('{x}', "" + zxy[1])
.replace('{y}', "" + zxy[2])
.replace('{z}', "" + z)
.replace('{x}', "" + x)
.replace('{y}', "" + y)
this.tileIndex = Utils.tile_index(z, x, y)
this.bbox = BBox.fromTile(z, x, y)
} else {
this.tileIndex = Utils.tile_index(0, 0, 0)
this.bbox = BBox.global;
}
this.name = "GeoJsonSource of " + url;

View file

@ -1,9 +1,9 @@
import FeatureSource from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import {OsmObject} from "../Osm/OsmObject";
import {Utils} from "../../Utils";
import Loc from "../../Models/Loc";
import FilteredLayer from "../../Models/FilteredLayer";
import FeatureSource from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc";
import FilteredLayer from "../../../Models/FilteredLayer";
import {Utils} from "../../../Utils";
import {OsmObject} from "../../Osm/OsmObject";
export default class OsmApiFeatureSource implements FeatureSource {

View file

@ -1,11 +1,10 @@
import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import FilteredLayer from "../../Models/FilteredLayer";
/**
* Every previously added point is remembered, but new points are added.
* Data coming from upstream will always overwrite a previous value
*/
import FeatureSource from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
export default class RememberingSource implements FeatureSource {
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>;

View file

@ -1,6 +1,6 @@
import {FeatureSourceForLayer} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import FilteredLayer from "../../Models/FilteredLayer";
import {UIEventSource} from "../../UIEventSource";
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer} from "../FeatureSource";
export default class SimpleFeatureSource implements FeatureSourceForLayer {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);

View file

@ -8,12 +8,19 @@ export default class StaticFeatureSource implements FeatureSource {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name: string = "StaticFeatureSource"
constructor(features: any[]) {
constructor(features: any[] | UIEventSource<any[]>, useFeaturesDirectly = false) {
const now = new Date();
this.features = new UIEventSource(features.map(f => ({
feature: f,
freshness: now
})))
if(useFeaturesDirectly){
// @ts-ignore
this.features = features
}else if (features instanceof UIEventSource) {
this.features = features.map(features => features.map(f => ({feature: f, freshness: now})))
} else {
this.features = new UIEventSource(features.map(f => ({
feature: f,
freshness: now
})))
}
}

View file

@ -1,18 +1,18 @@
import {FeatureSourceForLayer} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import {GeoOperations} from "../GeoOperations";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
/**
* This is the part of the pipeline which introduces extra points at the center of an area (but only if this is demanded by the wayhandling)
*/
import {UIEventSource} from "../../UIEventSource";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import {GeoOperations} from "../../GeoOperations";
import {FeatureSourceForLayer} from "../FeatureSource";
export default class WayHandlingApplyingFeatureSource implements FeatureSourceForLayer {
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
public readonly name;
public readonly layer;
constructor(upstream: FeatureSourceForLayer) {
this.name = "Wayhandling of " + upstream.name;
this.name = "Wayhandling(" + upstream.name+")";
this.layer = upstream.layer
const layer = upstream.layer.layerDef;

View file

@ -0,0 +1,63 @@
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc";
import DynamicTileSource from "./DynamicTileSource";
import {Utils} from "../../../Utils";
import GeoJsonSource from "../Sources/GeoJsonSource";
export default class DynamicGeoJsonTileSource extends DynamicTileSource {
constructor(layer: FilteredLayer,
registerLayer: (layer: FeatureSourceForLayer) => void,
state: {
locationControl: UIEventSource<Loc>
leafletMap: any
}) {
const source = layer.layerDef.source
if (source.geojsonZoomLevel === undefined) {
throw "Invalid layer: geojsonZoomLevel expected"
}
if (source.geojsonSource === undefined) {
throw "Invalid layer: geojsonSource expected"
}
const whitelistUrl = source.geojsonSource.replace("{z}_{x}_{y}.geojson", "overview.json")
.replace("{layer}",layer.layerDef.id)
let whitelist = undefined
Utils.downloadJson(whitelistUrl).then(
json => {
const data = new Map<number, Set<number>>();
for (const x in json) {
data.set(Number(x), new Set(json[x]))
}
whitelist = data
}
).catch(err => {
console.warn("No whitelist found for ", layer.layerDef.id, err)
})
super(
layer,
source.geojsonZoomLevel,
(zxy) => {
if(whitelist !== undefined){
const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2])
if(!isWhiteListed){
return undefined;
}
}
const src = new GeoJsonSource(
layer,
zxy
)
registerLayer(src)
return src
},
state
);
}
}

View file

@ -1,22 +1,24 @@
/***
* A tiled source which dynamically loads the required tiles
*/
import State from "../../../State";
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer} from "../FeatureSource";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {Utils} from "../../../Utils";
import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc";
import TileHierarchy from "./TileHierarchy";
export default class DynamicTileSource {
/***
* A tiled source which dynamically loads the required tiles at a fixed zoom level
*/
export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
private readonly _loadedTiles = new Set<number>();
public readonly existingTiles: Map<number, Map<number, FeatureSourceForLayer>> = new Map<number, Map<number, FeatureSourceForLayer>>()
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>;
constructor(
layer: FilteredLayer,
zoomlevel: number,
constructTile: (xy: [number, number]) => FeatureSourceForLayer,
constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled),
state: {
locationControl: UIEventSource<Loc>
leafletMap: any
@ -24,6 +26,8 @@ export default class DynamicTileSource {
) {
state = State.state
const self = this;
this.loadedTiles = new Map<number,FeatureSourceForLayer & Tiled>()
const neededTiles = state.locationControl.map(
location => {
if (!layer.isDisplayed.data) {
@ -45,28 +49,30 @@ export default class DynamicTileSource {
const tileRange = Utils.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
const needed = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i))
if(needed.length === 0){
if (needed.length === 0) {
return undefined
}
return needed
}
, [layer.isDisplayed, state.leafletMap]).stabilized(250);
neededTiles.addCallbackAndRunD(neededIndexes => {
console.log("Tiled geojson source ",layer.layerDef.id," needs", neededIndexes)
if (neededIndexes === undefined) {
return;
}
for (const neededIndex of neededIndexes) {
self._loadedTiles.add(neededIndex)
const xy = Utils.tile_from_index(zoomlevel, neededIndex)
const src = constructTile(xy)
let xmap = self.existingTiles.get(xy[0])
if(xmap === undefined){
xmap = new Map<number, FeatureSourceForLayer>()
self.existingTiles.set(xy[0], xmap)
const src = constructTile( Utils.tile_from_index(neededIndex))
if(src !== undefined){
self.loadedTiles.set(neededIndex, src)
}
xmap.set(xy[1], src)
}
})
}
}
}

View file

@ -1,3 +1,27 @@
Data in MapComplete can come from multiple sources.
In order to keep thins snappy, they are distributed over a tiled database
Currently, they are:
- The Overpass-API
- The OSM-API
- One or more GeoJSON files. This can be a single file or a set of tiled geojson files
- LocalStorage, containing features from a previous visit
- Changes made by the user introducing new features
When the data enters from Overpass or from the OSM-API, they are first distributed per layer:
OVERPASS | ---PerLayerFeatureSource---> FeatureSourceForLayer[]
OSM |
The GeoJSon files (not tiled) are then added to this list
A single FeatureSourcePerLayer is then further handled by splitting it into a tile hierarchy.
In order to keep thins snappy, they are distributed over a tiled database per layer.
## Notes
`cached-featuresbookcases` is the old key used `cahced-features{themeid}` and should be cleaned up

View file

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

View file

@ -1,10 +1,10 @@
import TileHierarchy from "./TiledFeatureSource/TileHierarchy";
import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource";
import {UIEventSource} from "../UIEventSource";
import FilteredLayer from "../../Models/FilteredLayer";
import FeatureSourceMerger from "./Sources/FeatureSourceMerger";
import {BBox} from "../GeoOperations";
import {Utils} from "../../Utils";
import TileHierarchy from "./TileHierarchy";
import {UIEventSource} from "../../UIEventSource";
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import FilteredLayer from "../../../Models/FilteredLayer";
import {Utils} from "../../../Utils";
import {BBox} from "../../GeoOperations";
import FeatureSourceMerger from "../Sources/FeatureSourceMerger";
export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
@ -24,8 +24,9 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer
* @param src
* @param index
*/
public registerTile(src: FeatureSource, index: number) {
public registerTile(src: FeatureSource & Tiled) {
const index = src.tileIndex
if (this.sources.has(index)) {
const sources = this.sources.get(index)
sources.data.push(src)

View file

@ -0,0 +1,191 @@
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
import {Utils} from "../../../Utils";
import {BBox} from "../../GeoOperations";
import FilteredLayer from "../../../Models/FilteredLayer";
import TileHierarchy from "./TileHierarchy";
import {feature} from "@turf/turf";
/**
* Contains all features in a tiled fashion.
* The data will be automatically broken down into subtiles when there are too much features in a single tile or if the zoomlevel is too high
*/
export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, FeatureSourceForLayer, TileHierarchy<IndexedFeatureSource & FeatureSourceForLayer & Tiled> {
public readonly z: number;
public readonly x: number;
public readonly y: number;
public readonly parent: TiledFeatureSource;
public readonly root: TiledFeatureSource
public readonly layer: FilteredLayer;
/* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile.
* Only defined on the root element!
*/
public readonly loadedTiles: Map<number, TiledFeatureSource & FeatureSourceForLayer> = undefined;
public readonly maxFeatureCount: number;
public readonly name;
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>
public readonly containedIds: UIEventSource<Set<string>>
public readonly bbox: BBox;
private upper_left: TiledFeatureSource
private upper_right: TiledFeatureSource
private lower_left: TiledFeatureSource
private lower_right: TiledFeatureSource
private readonly maxzoom: number;
private readonly options: TiledFeatureSourceOptions
public readonly tileIndex: number;
private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) {
this.z = z;
this.x = x;
this.y = y;
this.bbox = BBox.fromTile(z, x, y)
this.tileIndex = Utils.tile_index(z, x, y)
this.name = `TiledFeatureSource(${z},${x},${y})`
this.parent = parent;
this.layer = options.layer
options = options ?? {}
this.maxFeatureCount = options?.maxFeatureCount ?? 500;
this.maxzoom = options.maxZoomLevel ?? 18
this.options = options;
if (parent === undefined) {
throw "Parent is not allowed to be undefined. Use null instead"
}
if (parent === null && z !== 0 && x !== 0 && y !== 0) {
throw "Invalid root tile: z, x and y should all be null"
}
if (parent === null) {
this.root = this;
this.loadedTiles = new Map()
} else {
this.root = this.parent.root;
this.loadedTiles = this.root.loadedTiles;
const i = Utils.tile_index(z, x, y)
this.root.loadedTiles.set(i, this)
}
this.features = new UIEventSource<any[]>([])
this.containedIds = this.features.map(features => {
if (features === undefined) {
return undefined;
}
return new Set(features.map(f => f.feature.properties.id))
})
// We register this tile, but only when there is some data in it
if (this.options.registerTile !== undefined) {
this.features.addCallbackAndRunD(features => {
if (features.length === 0) {
return;
}
this.options.registerTile(this)
return true;
})
}
}
public static createHierarchy(features: FeatureSource, options?: TiledFeatureSourceOptions): TiledFeatureSource {
const root = new TiledFeatureSource(0, 0, 0, null, options)
features.features?.addCallbackAndRunD(feats => root.addFeatures(feats))
return root;
}
private isSplitNeeded(featureCount: number){
if(this.upper_left !== undefined){
// This tile has been split previously, so we keep on splitting
return true;
}
if(this.z >= this.maxzoom){
// We are not allowed to split any further
return false
}
if(this.options.minZoomLevel !== undefined && this.z < this.options.minZoomLevel){
// We must have at least this zoom level before we are allowed to start splitting
return true
}
// To much features - we split
return featureCount > this.maxFeatureCount
}
/***
* Adds the list of features to this hierarchy.
* If there are too much features, the list will be broken down and distributed over the subtiles (only retaining features that don't fit a subtile on this level)
* @param features
* @private
*/
private addFeatures(features: { feature: any, freshness: Date }[]) {
if (features === undefined || features.length === 0) {
return;
}
if (!this.isSplitNeeded(features.length)) {
this.features.setData(features)
return;
}
if (this.upper_left === undefined) {
this.upper_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2, this, this.options)
this.upper_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2, this, this.options)
this.lower_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2 + 1, this, this.options)
this.lower_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2 + 1, this, this.options)
}
const ulf = []
const urf = []
const llf = []
const lrf = []
const overlapsboundary = []
for (const feature of features) {
const bbox = BBox.get(feature.feature)
if (this.options.minZoomLevel === undefined) {
if (bbox.isContainedIn(this.upper_left.bbox)) {
ulf.push(feature)
} else if (bbox.isContainedIn(this.upper_right.bbox)) {
urf.push(feature)
} else if (bbox.isContainedIn(this.lower_left.bbox)) {
llf.push(feature)
} else if (bbox.isContainedIn(this.lower_right.bbox)) {
lrf.push(feature)
} else {
overlapsboundary.push(feature)
}
} else {
// We duplicate a feature on a boundary into every tile as we need to get to the minZoomLevel
if (bbox.overlapsWith(this.upper_left.bbox)) {
ulf.push(feature)
}
if (bbox.overlapsWith(this.upper_right.bbox)) {
urf.push(feature)
}
if (bbox.overlapsWith(this.lower_left.bbox)) {
llf.push(feature)
}
if (bbox.overlapsWith(this.lower_right.bbox)) {
lrf.push(feature)
}
}
}
this.upper_left.addFeatures(ulf)
this.upper_right.addFeatures(urf)
this.lower_left.addFeatures(llf)
this.lower_right.addFeatures(lrf)
this.features.setData(overlapsboundary)
}
}
export interface TiledFeatureSourceOptions {
readonly maxFeatureCount?: number,
readonly maxZoomLevel?: number,
readonly minZoomLevel?: number,
readonly registerTile?: (tile: TiledFeatureSource & Tiled) => void,
readonly layer?: FilteredLayer
}

View file

@ -1,40 +1,102 @@
import FilteredLayer from "../../../Models/FilteredLayer";
import {FeatureSourceForLayer} from "../FeatureSource";
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
import {UIEventSource} from "../../UIEventSource";
import Loc from "../../../Models/Loc";
import GeoJsonSource from "../GeoJsonSource";
import DynamicTileSource from "./DynamicTileSource";
import TileHierarchy from "./TileHierarchy";
import {Utils} from "../../../Utils";
import LocalStorageSaverActor from "../Actors/LocalStorageSaverActor";
import {BBox} from "../../GeoOperations";
export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
public loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
export default class DynamicGeoJsonTileSource extends DynamicTileSource {
constructor(layer: FilteredLayer,
registerLayer: (layer: FeatureSourceForLayer) => void,
handleFeatureSource: (src: FeatureSourceForLayer & Tiled, index: number) => void,
state: {
locationControl: UIEventSource<Loc>
leafletMap: any
}) {
const source = layer.layerDef.source
if (source.geojsonZoomLevel === undefined) {
throw "Invalid layer: geojsonZoomLevel expected"
}
if (source.geojsonSource === undefined) {
throw "Invalid layer: geojsonSource expected"
}
super(
layer,
source.geojsonZoomLevel,
(xy) => {
const xyz: [number, number, number] = [xy[0], xy[1], source.geojsonZoomLevel]
const src = new GeoJsonSource(
layer,
xyz
)
registerLayer(src)
return src
},
state
);
const prefix = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-"
// @ts-ignore
const indexes: number[] = Object.keys(localStorage)
.filter(key => {
return key.startsWith(prefix) && !key.endsWith("-time");
})
.map(key => {
return Number(key.substring(prefix.length));
})
console.log("Avaible datasets in local storage:", indexes)
const zLevels = indexes.map(i => i % 100)
const indexesSet = new Set(indexes)
const maxZoom = Math.max(...zLevels)
const minZoom = Math.min(...zLevels)
const self = this;
const neededTiles = state.locationControl.map(
location => {
if (!layer.isDisplayed.data) {
// No need to download! - the layer is disabled
return undefined;
}
if (location.zoom < layer.layerDef.minzoom) {
// No need to download! - the layer is disabled
return undefined;
}
// Yup, this is cheating to just get the bounds here
const bounds = state.leafletMap.data?.getBounds()
if (bounds === undefined) {
// We'll retry later
return undefined
}
const needed = []
for (let z = minZoom; z <= maxZoom; z++) {
const tileRange = Utils.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
const neededZ = Utils.MapRange(tileRange, (x, y) => Utils.tile_index(z, x, y))
.filter(i => !self.loadedTiles.has(i) && indexesSet.has(i))
needed.push(...neededZ)
}
if (needed.length === 0) {
return undefined
}
return needed
}
, [layer.isDisplayed, state.leafletMap]).stabilized(50);
neededTiles.addCallbackAndRun(t => console.log("Tiles to load from localstorage:", t))
neededTiles.addCallbackAndRunD(neededIndexes => {
for (const neededIndex of neededIndexes) {
// We load the features from localStorage
try {
const key = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-" + neededIndex
const data = localStorage.getItem(key)
const features = JSON.parse(data)
const src = {
layer: layer,
features: new UIEventSource<{ feature: any; freshness: Date }[]>(features),
name: "FromLocalStorage(" + key + ")",
tileIndex: neededIndex,
bbox: BBox.fromTile(...Utils.tile_from_index(neededIndex))
}
handleFeatureSource(src, neededIndex)
self.loadedTiles.set(neededIndex, src)
} catch (e) {
console.error("Could not load data tile from local storage due to", e)
}
}
})
}
}

View file

@ -1,4 +1,5 @@
import * as turf from '@turf/turf'
import {Utils} from "../Utils";
export class GeoOperations {
@ -184,6 +185,44 @@ export class GeoOperations {
static lengthInMeters(feature: any) {
return turf.length(feature) * 1000
}
static buffer(feature: any, bufferSizeInMeter: number){
return turf.buffer(feature, bufferSizeInMeter/1000, {
units: 'kilometers'
})
}
static bbox(feature: any){
const [lon, lat, lon0, lat0] = turf.bbox(feature)
return {
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[
lon,
lat
],
[
lon0,
lat
],
[
lon0,
lat0
],
[
lon,
lat0
],
[
lon,
lat
],
]
}
}
}
/**
* Generates the closest point on a way from a given point
@ -340,6 +379,7 @@ export class BBox {
readonly maxLon: number;
readonly minLat: number;
readonly minLon: number;
static global: BBox = new BBox([[-180,-90],[180,90]]);
constructor(coordinates) {
this.maxLat = Number.MIN_VALUE;
@ -361,12 +401,11 @@ export class BBox {
return new BBox([[bounds.getWest(), bounds.getNorth()], [bounds.getEast(), bounds.getSouth()]])
}
static get(feature) {
static get(feature): BBox {
if (feature.bbox?.overlapsWith === undefined) {
const turfBbox: number[] = turf.bbox(feature)
feature.bbox = new BBox([[turfBbox[0], turfBbox[1]], [turfBbox[2], turfBbox[3]]]);
}
return feature.bbox;
}
@ -407,4 +446,23 @@ export class BBox {
}
}
static fromTile(z: number, x: number, y: number) {
return new BBox( Utils.tile_bounds_lon_lat(z, x, y))
}
getEast() {
return this.maxLon
}
getNorth() {
return this.maxLat
}
getWest() {
return this.minLon
}
getSouth() {
return this.minLat
}
}

View file

@ -32,7 +32,6 @@ export class Mapillary extends ImageAttributionSource {
}
const mapview = value.match(/https?:\/\/www.mapillary.com\/map\/im\/(.*)/)
console.log("Mapview matched ", value, mapview)
if(mapview !== null){
const key = mapview[1]
return {key:key, isApiv4: !isNaN(Number(key))};

View file

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

View file

@ -1,15 +1,30 @@
/**
* Represents a single change to an object
*/
export interface ChangeDescription {
/**
* Identifier of the object
*/
type: "node" | "way" | "relation",
/**
* Negative for a new objects
* Identifier of the object
* Negative for new objects
*/
id: number,
/*
v = "" or v = undefined to erase this tag
*/
/**
* All changes to tags
* v = "" or v = undefined to erase this tag
*/
tags?: { k: string, v: string }[],
/**
* A change to the geometry:
* 1) Change of node location
* 2) Change of way geometry
* 3) Change of relation members (untested)
*/
changes?: {
lat: number,
lon: number

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import State from "../../State";
import {UIEventSource} from "../UIEventSource";
export interface Relation {
id: number,
@ -13,11 +14,15 @@ export interface Relation {
properties: any
}
export default class ExtractRelations {
export default class RelationsTracker {
public static RegisterRelations(overpassJson: any): void {
const memberships = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(overpassJson))
State.state.knownRelations.setData(memberships)
public knownRelations = new UIEventSource<Map<string, { role: string; relation: Relation }[]>>(new Map(), "Relation memberships");
constructor() {
}
public RegisterRelations(overpassJson: any): void {
this.UpdateMembershipTable(RelationsTracker.GetRelationElements(overpassJson))
}
/**
@ -25,7 +30,7 @@ export default class ExtractRelations {
* @param overpassJson
* @constructor
*/
public static GetRelationElements(overpassJson: any): Relation[] {
private static GetRelationElements(overpassJson: any): Relation[] {
const relations = overpassJson.elements
.filter(element => element.type === "relation" && element.tags.type !== "multipolygon")
for (const relation of relations) {
@ -39,12 +44,11 @@ export default class ExtractRelations {
* @param relations
* @constructor
*/
public static BuildMembershipTable(relations: Relation[]): Map<string, { role: string, relation: Relation }[]> {
const memberships = new Map<string, { role: string, relation: Relation }[]>()
private UpdateMembershipTable(relations: Relation[]): void {
const memberships = this.knownRelations.data
let changed = false;
for (const relation of relations) {
for (const member of relation.members) {
const role = {
role: member.role,
relation: relation
@ -53,11 +57,21 @@ export default class ExtractRelations {
if (!memberships.has(key)) {
memberships.set(key, [])
}
memberships.get(key).push(role)
const knownRelations = memberships.get(key)
const alreadyExists = knownRelations.some(knownRole => {
return knownRole.role === role.role && knownRole.relation === role.relation
})
if (!alreadyExists) {
knownRelations.push(role)
changed = true;
}
}
}
if (changed) {
this.knownRelations.ping()
}
return memberships
}
}

View file

@ -9,6 +9,7 @@ import Combine from "../UI/Base/Combine";
import BaseUIElement from "../UI/BaseUIElement";
import Title from "../UI/Base/Title";
import {FixedUiElement} from "../UI/Base/FixedUiElement";
import CountryCoder from "latlon2country/index";
const cardinalDirections = {
@ -20,7 +21,7 @@ const cardinalDirections = {
export default class SimpleMetaTagger {
static coder: any;
private static coder: CountryCoder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
public static readonly objectMetaInfo = new SimpleMetaTagger(
{
keys: ["_last_edit:contributor",
@ -84,7 +85,7 @@ export default class SimpleMetaTagger {
},
(feature => {
const units = Utils.NoNull([].concat(...State.state?.layoutToUse?.data?.layers?.map(layer => layer.units ?? [])));
if(units.length == 0){
if (units.length == 0) {
return;
}
let rewritten = false;
@ -93,7 +94,7 @@ export default class SimpleMetaTagger {
continue;
}
for (const unit of units) {
if(unit.appliesToKeys === undefined){
if (unit.appliesToKeys === undefined) {
console.error("The unit ", unit, "has no appliesToKey defined")
continue
}
@ -148,7 +149,7 @@ export default class SimpleMetaTagger {
const lat = centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0];
SimpleMetaTagger.GetCountryCodeFor(lon, lat, (countries) => {
SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, (countries: string[]) => {
try {
const oldCountry = feature.properties["_country"];
feature.properties["_country"] = countries[0].trim().toLowerCase();
@ -160,7 +161,7 @@ export default class SimpleMetaTagger {
} catch (e) {
console.warn(e)
}
});
})
}
)
private static isOpen = new SimpleMetaTagger(
@ -426,11 +427,7 @@ export default class SimpleMetaTagger {
}
}
static GetCountryCodeFor(lon: number, lat: number, callback: (country: string) => void) {
SimpleMetaTagger.coder?.GetCountryCodeFor(lon, lat, callback)
}
static HelpText(): BaseUIElement {
public static HelpText(): BaseUIElement {
const subElements: (string | BaseUIElement)[] = [
new Combine([
new Title("Metatags", 1),
@ -453,7 +450,7 @@ export default class SimpleMetaTagger {
return new Combine(subElements).SetClass("flex-col")
}
addMetaTags(features: { feature: any, freshness: Date }[]) {
public addMetaTags(features: { feature: any, freshness: Date }[]) {
for (let i = 0; i < features.length; i++) {
let feature = features[i];
this._f(feature.feature, i, feature.freshness);

View file

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

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants {
public static vNumber = "0.9.12";
public static vNumber = "0.10.0";
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
@ -26,12 +26,6 @@ export default class Constants {
*/
static updateTimeoutSec: number = 30;
/**
* If zoom >= useOsmApiAt, then the OSM api will be used directly.
* If undefined, use overpass exclusively
*/
static useOsmApiAt = undefined;
private static isRetina(): boolean {
if (Utils.runningFromConsole) {
return;

View file

@ -59,10 +59,9 @@ export interface LayerConfigJson {
* NOTE: the previous format was 'overpassTags: AndOrTagConfigJson | string', which is interpreted as a shorthand for source: {osmTags: "key=value"}
* While still supported, this is considered deprecated
*/
source: { osmTags: AndOrTagConfigJson | string } |
{ osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean } |
{ osmTags: AndOrTagConfigJson | string, overpassScript: string }
source: { osmTags: AndOrTagConfigJson | string, overpassScript?: string } |
{ osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number, isOsmCache?: boolean }
/**
*
* A list of extra tags to calculate, specified as "keyToAssignTo=javascript-expression".

View file

@ -246,14 +246,6 @@ export default class LayoutConfig {
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
* This is to fix loading from external sources

View file

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

View file

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

View file

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

View file

@ -1,208 +1,30 @@
import BaseUIElement from "../BaseUIElement";
import * as L from "leaflet";
import {Map} from "leaflet";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import {Utils} from "../../Utils";
import {BBox} from "../../Logic/GeoOperations";
import {UIEventSource} from "../../Logic/UIEventSource";
export default class Minimap extends BaseUIElement {
export interface MinimapOptions {
background?: UIEventSource<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 }>
}
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 }>;
export default class Minimap {
/**
* A stub implementation. The actual implementation is injected later on, but only in the browser.
* importing leaflet crashes node-ts, which is pretty annoying considering the fact that a lot of scripts use it
*/
constructor(options?: {
background?: UIEventSource<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++
/**
* Construct a minimap
*/
public static createMiniMap: (options: MinimapOptions) => BaseUIElement & { readonly leafletMap: UIEventSource<any> }
}
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",
background: State.state.backgroundLayer,
location: State.state.locationControl,
features: State.state.featurePipeline.features,
features: State.state.featurePipeline,
layout: State.state.layoutToUse,
}).isRunning.addCallbackAndRun(isRunning => isExporting.setData(isRunning))
}

View file

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

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.credits,
"<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/>",
new VariableUiElement(contributions.map(contributions => {
if(contributions === undefined){
return ""
}
const sorted = Array.from(contributions, ([name, value]) => ({
name,
value

View file

@ -2,54 +2,113 @@ import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import State from "../../State";
import {FeatureSourceUtils} from "../../Logic/FeatureSource/FeatureSource";
import {Utils} from "../../Utils";
import Combine from "../Base/Combine";
import CheckBoxes from "../Input/Checkboxes";
import {GeoOperations} from "../../Logic/GeoOperations";
import {BBox, GeoOperations} from "../../Logic/GeoOperations";
import Toggle from "../Input/Toggle";
import Title from "../Base/Title";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {UIEventSource} from "../../Logic/UIEventSource";
import SimpleMetaTagger from "../../Logic/SimpleMetaTagger";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {meta} from "@turf/turf";
export class DownloadPanel extends Toggle {
constructor() {
const state: {
featurePipeline: FeaturePipeline,
layoutToUse: UIEventSource<LayoutConfig>,
currentBounds: UIEventSource<BBox>
} = State.state
const t = Translations.t.general.download
const somethingLoaded = State.state.featurePipeline.features.map(features => features.length > 0);
const name = State.state.layoutToUse.data.id;
const includeMetaToggle = new CheckBoxes([t.includeMetaData.Clone()])
const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0)
const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
new Combine([t.downloadGeojson.Clone().SetClass("font-bold"),
t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col"))
.onClick(() => {
const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data})
const name = State.state.layoutToUse.data.id;
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson),
const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "),
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, {
mimetype: "application/vnd.geo+json"
});
})
const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine(
[t.downloadCSV.Clone().SetClass("font-bold"),
t.downloadCSVHelper.Clone()]).SetClass("flex flex-col"))
.onClick(() => {
const geojson = FeatureSourceUtils.extractGeoJson(State.state.featurePipeline, {metadata: metaisIncluded.data})
const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
const csv = GeoOperations.toCSV(geojson.features)
const name = State.state.layoutToUse.data.id;
Utils.offerContentsAsDownloadableFile(csv,
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, {
mimetype: "text/csv"
});
})
const downloadButtons = new Combine(
[new Title(t.title), buttonGeoJson, buttonCSV, includeMetaToggle, t.licenseInfo.Clone().SetClass("link-underline")])
[new Title(t.title),
buttonGeoJson,
buttonCSV,
includeMetaToggle,
t.licenseInfo.Clone().SetClass("link-underline")])
.SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
super(
downloadButtons,
t.noDataLoaded.Clone(),
somethingLoaded)
state.featurePipeline.somethingLoaded)
}
private static getCleanGeoJson(state: {
featurePipeline: FeaturePipeline,
currentBounds: UIEventSource<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 Toggle from "../Input/Toggle";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Utils} from "../../Utils";
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
@ -62,9 +63,15 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)]
const now = new Date()
const date = now.getFullYear()+"-"+Utils.TwoDigits(now.getMonth()+1)+"-"+Utils.TwoDigits(now.getDate())
const osmcha_link = `https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%22${date}%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D`
tabsWithAboutMc.push({
header: Svg.help,
content: new Combine([Translations.t.general.aboutMapcomplete.Clone(), "<br/>Version " + Constants.vNumber])
content: new Combine([Translations.t.general.aboutMapcomplete.Clone()
.Subs({"osmcha_link": osmcha_link}), "<br/>Version " + Constants.vNumber])
.SetClass("link-underline")
}
);

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {SubtleButton} from "../Base/SubtleButton";
import Minimap from "../Base/Minimap";
import State from "../../State";
import ShowDataLayer from "../ShowDataLayer";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import {GeoOperations} from "../../Logic/GeoOperations";
import {LeafletMouseEvent} from "leaflet";
import Combine from "../Base/Combine";
@ -13,10 +13,16 @@ import Translations from "../i18n/Translations";
import SplitAction from "../../Logic/Osm/Actions/SplitAction";
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
import Title from "../Base/Title";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
export default class SplitRoadWizard extends Toggle {
private static splitLayout = new UIEventSource(SplitRoadWizard.GetSplitLayout())
private static splitLayerStyling = new LayerConfig({
id: "splitpositions",
source: {osmTags: "_cutposition=yes"},
icon: "./assets/svg/plus.svg"
}, "(BUILTIN) SplitRoadWizard.ts", true)
/**
* A UI Element used for splitting roads
@ -36,7 +42,7 @@ export default class SplitRoadWizard extends Toggle {
const splitClicked = new UIEventSource<boolean>(false);
// Minimap on which you can select the points to be splitted
const miniMap = new Minimap({background: State.state.backgroundLayer, allowMoving: false});
const miniMap = Minimap.createMiniMap({background: State.state.backgroundLayer, allowMoving: false});
miniMap.SetStyle("width: 100%; height: 24rem;");
// Define how a cut is displayed on the map
@ -45,8 +51,20 @@ export default class SplitRoadWizard extends Toggle {
const roadElement = State.state.allElements.ContainingFeatures.get(id)
const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]);
// Datalayer displaying the road and the cut points (if any)
new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true);
new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false)
new ShowDataMultiLayer({
features: new StaticFeatureSource(roadEventSource, true),
layers: State.state.filteredLayers,
leafletMap: miniMap.leafletMap,
enablePopups: false,
zoomToFeatures: true
})
new ShowDataLayer({
features: new StaticFeatureSource(splitPoints, true),
leafletMap: miniMap.leafletMap,
zoomToFeatures: false,
enablePopups: false,
layerToShow: SplitRoadWizard.splitLayerStyling
})
/**
* Handles a click on the overleaf map.
@ -135,21 +153,4 @@ export default class SplitRoadWizard extends Toggle {
const confirm = new Toggle(mapView, splitToggle, splitClicked);
super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit)
}
private static GetSplitLayout(): LayoutConfig {
return new LayoutConfig({
maintainer: "mapcomplete",
language: ["en"],
startLon: 0,
startLat: 0,
description: "Split points visualisations - built in at SplitRoadWizard.ts",
icon: "", startZoom: 0,
title: "Split locations",
version: "",
id: "splitpositions",
layers: [{id: "splitpositions", source: {osmTags: "_cutposition=yes"}, icon: "./assets/svg/plus.svg"}]
}, true, "(BUILTIN) SplitRoadWizard.ts")
}
}

View file

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

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
*/
import {UIEventSource} from "../../Logic/UIEventSource";
import ShowDataLayer from "./ShowDataLayer";
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
import FilteredLayer from "../../Models/FilteredLayer";
import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
export default class ShowDataMultiLayer {
constructor(options: ShowDataLayerOptions & { layers: UIEventSource<FilteredLayer[]> }) {

View file

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

View file

@ -245,7 +245,6 @@ export class Utils {
}
dict.set(k, v());
return dict.get(k);
}
/**
@ -259,6 +258,26 @@ export class Utils {
return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]]
}
static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] {
return [[Utils.tile2long(x, z),Utils.tile2lat(y, z)], [Utils.tile2long(x + 1, z), Utils.tile2lat(y + 1, z)]]
}
static tile_index(z: number, x: number, y: number):number{
return ((x * (2 << z)) + y) * 100 + z
}
/**
* Given a tile index number, returns [z, x, y]
* @param index
* @returns 'zxy'
*/
static tile_from_index(index: number) : [number, number, number]{
const z = index % 100;
const factor = 2 << z
index = Math.floor(index / 100)
return [z, Math.floor(index / factor), index % factor]
}
/**
* Return x, y of the tile containing (lat, lon) on the given zoom level
*/
@ -422,13 +441,6 @@ export class Utils {
return bestColor ?? hex;
}
public static setDefaults(options, defaults) {
for (let key in defaults) {
if (!(key in options)) options[key] = defaults[key];
}
return options;
}
private static tile2long(x, z) {
return (x / Math.pow(2, z) * 360 - 180);
}

View file

@ -48,7 +48,7 @@
}
},
"calculatedTags": [
"_closest_other_drinking_water_id=feat.closest('drinking_water').id",
"_closest_other_drinking_water_id=feat.closest('drinking_water')?.id",
"_closest_other_drinking_water_distance=Math.floor(feat.distanceTo(feat.closest('drinking_water')) * 1000)"
],
"minzoom": 13,

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,
"startLon": 0,
"startZoom": 1,
"widenFactor": 0.05,
"widenFactor": 1,
"roamingRenderings": [],
"layers": [
"public_bookcase"

View file

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

View file

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

View file

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

View file

@ -131,7 +131,7 @@
]
},
"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 Combine from "./UI/Base/Combine";
import Translations from "./UI/i18n/Translations";
import CountryCoder from "latlon2country"
import SimpleMetaTagger from "./Logic/SimpleMetaTagger";
import Minimap from "./UI/Base/Minimap";
import DirectionInput from "./UI/Input/DirectionInput";
import SpecialVisualizations from "./UI/SpecialVisualizations";
import ShowDataLayer from "./UI/ShowDataLayer";
import * as L from "leaflet";
import ValidatedTextField from "./UI/Input/ValidatedTextField";
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
import Constants from "./Models/Constants";
import MinimapImplementation from "./UI/Base/MinimapImplementation";
MinimapImplementation.initialize()
// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
DirectionInput.constructMinimap = options => new Minimap(options)
ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)
SpecialVisualizations.constructMiniMap = options => new Minimap(options)
SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>,
leafletMap: UIEventSource<L.Map>,
layoutToUse: UIEventSource<LayoutConfig>,
enablePopups = true,
zoomToFeatures = false) => new ShowDataLayer(features, leafletMap, layoutToUse, enablePopups, zoomToFeatures)
let defaultLayout = ""
// --------------------- Special actions based on the parameters -----------------

View file

@ -159,7 +159,7 @@
"noTagsSelected": "No tags selected",
"testing": "Testing - changes won't be saved",
"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",
"openTheMap": "Open the map",
"loginOnlyNeededToEdit": "if you want to edit the map",

View file

@ -3021,6 +3021,29 @@
}
},
"toilet": {
"filter": {
"0": {
"options": {
"0": {
"question": "Wheelchair accessible"
}
}
},
"1": {
"options": {
"0": {
"question": "Has a changing table"
}
}
},
"2": {
"options": {
"0": {
"question": "Free to use"
}
}
}
},
"name": "Toilets",
"presets": {
"0": {

View file

@ -192,7 +192,7 @@
"getStartedNewAccount": " of <a href='https://www.openstreetmap.org/user/new' target='_blank'>maak een nieuwe account aan</a>",
"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.",
"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",
"layerSelection": {
"zoomInToSeeThisLayer": "Vergroot de kaart om deze laag te zien",

View file

@ -1351,6 +1351,11 @@
"title": {
"render": "Known address"
}
},
"2": {
"title": {
"render": "{name}"
}
}
},
"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",
"generate:layouts": "ts-node scripts/generateLayouts.ts",
"generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts",
"generate:cache:speelplekken:mini": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.181710380278176 4.423413276672363 51.193007664772495 4.444141387939452",
"generate:cache:speelplekken:mini": "ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache_mini/ 51.181710380278176 4.423413276672363 51.193007664772495 4.444141387939452",
"generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../pietervdvn.github.io/speelplekken_cache/ 51.20 4.35 51.09 4.56",
"generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../pietervdvn.github.io/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre",
"generate:layeroverview": "npm run generate:licenses && echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail",
"generate:layeroverview": "echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json && ts-node scripts/generateLayerOverview.ts --no-fail",
"generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail",
"query:licenses": "ts-node scripts/generateLicenseInfo.ts --query",
"generate:report": "cd Docs/Tools && ./compileStats.sh && git commit . -m 'New statistics ands graphs' && git push",

View file

@ -7,7 +7,6 @@ import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
Utils.runningFromConsole = true
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
*/
import {Utils} from "../Utils";
Utils.runningFromConsole = true
import {Overpass} from "../Logic/Osm/Overpass";
import * as fs from "fs";
import {existsSync, readFileSync, writeFileSync} from "fs";
import {TagsFilter} from "../Logic/Tags/TagsFilter";
import {Or} from "../Logic/Tags/Or";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import ExtractRelations from "../Logic/Osm/ExtractRelations";
import RelationsTracker from "../Logic/Osm/RelationsTracker";
import * as OsmToGeoJson from "osmtogeojson";
import MetaTagging from "../Logic/MetaTagging";
import {GeoOperations} from "../Logic/GeoOperations";
import {UIEventSource} from "../Logic/UIEventSource";
import {TileRange} from "../Models/TileRange";
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import ScriptUtils from "./ScriptUtils";
import PerLayerFeatureSourceSplitter from "../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
import FilteredLayer from "../Models/FilteredLayer";
import FeatureSource, {FeatureSourceForLayer} from "../Logic/FeatureSource/FeatureSource";
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource";
ScriptUtils.fixUtils()
function createOverpassObject(theme: LayoutConfig) {
function createOverpassObject(theme: LayoutConfig, relationTracker: RelationsTracker) {
let filters: TagsFilter[] = [];
let extraScripts: string[] = [];
for (const layer of theme.layers) {
@ -54,7 +59,7 @@ function createOverpassObject(theme: LayoutConfig) {
throw "Nothing to download! The theme doesn't declare anything to download"
}
return new Overpass(new Or(filters), extraScripts, new UIEventSource<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 {
@ -75,7 +80,7 @@ async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/
downloaded++;
const filename = rawJsonName(targetdir, x, y, r.zoomlevel)
if (existsSync(filename)) {
console.log("Already exists: ", filename)
console.log("Already exists (not downloading again): ", filename)
skipped++
continue;
}
@ -145,14 +150,16 @@ async function downloadExtraData(theme: LayoutConfig)/* : any[] */ {
return allFeatures;
}
function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]) {
function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extraFeatures: any[]): FeatureSource {
let allFeatures = [...extraFeatures]
let processed = 0;
const layerIndex = theme.LayerIndex();
for (let x = r.xstart; x <= r.xend; x++) {
for (let y = r.ystart; y <= r.yend; y++) {
processed++;
const filename = rawJsonName(targetdir, x, y, r.zoomlevel)
ScriptUtils.erasableLog(" Post processing", processed, "/", r.total, filename)
console.log(" Loading and processing", processed, "/", r.total, filename)
if (!existsSync(filename)) {
console.error("Not found - and not downloaded. Run this script again!: " + filename)
continue;
@ -163,152 +170,97 @@ function postProcess(targetdir: string, r: TileRange, theme: LayoutConfig, extra
// Create and save the geojson file - which is the main chunk of the data
const geojson = OsmToGeoJson.default(rawOsm);
const osmTime = new Date(rawOsm.osm3s.timestamp_osm_base);
// And merge in the extra features - needed for the metatagging
geojson.features.push(...extraFeatures);
for (const feature of geojson.features) {
for (const layer of theme.layers) {
if (layer.source.osmTags.matchesProperties(feature.properties)) {
feature["_matching_layer_id"] = layer.id;
break;
}
}
}
const featuresFreshness = geojson.features.map(feature => {
return ({
freshness: osmTime,
feature: feature
});
});
// Extract the relationship information
const relations = ExtractRelations.BuildMembershipTable(ExtractRelations.GetRelationElements(rawOsm))
MetaTagging.addMetatags(featuresFreshness, new UIEventSource<{ feature: any; freshness: Date }[]>(featuresFreshness), relations, theme.layers, false);
for (const feature of geojson.features) {
const layer = layerIndex.get(feature["_matching_layer_id"])
if (layer === undefined) {
// Probably some extra, unneeded data, e.g. a point of a way
continue
}
if (layer.wayHandling == LayerConfig.WAYHANDLING_CENTER_ONLY) {
const centerpoint = GeoOperations.centerpointCoordinates(feature)
feature.geometry.type = "Point"
feature.geometry["coordinates"] = centerpoint;
}
}
for (const feature of geojson.features) {
// Some cleanup
delete feature["bbox"]
}
const targetPath = geoJsonName(targetdir + ".unfiltered", x, y, r.zoomlevel)
// This is the geojson file containing all features
writeFileSync(targetPath, JSON.stringify(geojson, null, " "))
allFeatures.push(...geojson.features)
}
}
return new StaticFeatureSource(allFeatures)
}
function splitPerLayer(targetdir: string, r: TileRange, theme: LayoutConfig) {
const z = r.zoomlevel;
const generated = {} // layer --> x --> y[]
for (let x = r.xstart; x <= r.xend; x++) {
for (let y = r.ystart; y <= r.yend; y++) {
const file = readFileSync(geoJsonName(targetdir + ".unfiltered", x, y, z), "UTF8")
/**
* Load all the tiles into memory from disk
*/
function postProcess(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string) {
for (const layer of theme.layers) {
if (!layer.source.isOsmCacheLayer) {
continue;
}
const geojson = JSON.parse(file)
const oldLength = geojson.features.length;
geojson.features = geojson.features
.filter(f => f._matching_layer_id === layer.id)
.filter(f => {
const isShown = layer.isShown.GetRenderValue(f.properties).txt
return isShown !== "no";
})
const new_path = geoJsonName(targetdir + "_" + layer.id, x, y, z);
ScriptUtils.erasableLog(new_path, " has ", geojson.features.length, " features after filtering (dropped ", oldLength - geojson.features.length, ")")
if (geojson.features.length == 0) {
continue;
function handleLayer(source: FeatureSourceForLayer) {
const layer = source.layer.layerDef;
const layerId = layer.id
if (layer.source.isOsmCacheLayer !== true) {
return;
}
console.log("Handling layer ", layerId, "which has", source.features.data.length, "features")
if (source.features.data.length === 0) {
return;
}
MetaTagging.addMetatags(source.features.data,
{
memberships: relationsTracker,
getFeaturesWithin: _ => {
return [allFeatures.features.data.map(f => f.feature)]
}
writeFileSync(new_path, JSON.stringify(geojson, null, " "))
},
layer,
false);
if (generated[layer.id] === undefined) {
generated[layer.id] = {}
const createdTiles = []
// At this point, we have all the features of the entire area.
// However, we want to export them per tile of a fixed size, so we use a dynamicTileSOurce to split it up
TiledFeatureSource.createHierarchy(source, {
minZoomLevel: 14,
maxZoomLevel: 14,
maxFeatureCount: undefined,
registerTile: tile => {
if (tile.z < 12) {
return;
}
if (generated[layer.id][x] === undefined) {
generated[layer.id][x] = []
if (tile.features.data.length === 0) {
return
}
generated[layer.id][x].push(y)
for (const feature of tile.features.data) {
// Some cleanup
delete feature.feature["bbox"]
}
// Lets save this tile!
const [z, x, y] = Utils.tile_from_index(tile.tileIndex)
console.log("Writing tile ", z, x, y, layerId)
const targetPath = geoJsonName(targetdir + "_" + layerId, x, y, z)
createdTiles.push(tile.tileIndex)
// This is the geojson file containing all features for this tile
writeFileSync(targetPath, JSON.stringify({
type: "FeatureCollection",
features: tile.features.data.map(f => f.feature)
}, null, " "))
}
}
}
for (const layer of theme.layers) {
const id = layer.id
const loaded = generated[id]
if (loaded === undefined) {
console.log("No features loaded for layer ", id)
continue;
}
writeFileSync(targetdir + "_" + id + "_overview.json", JSON.stringify(loaded))
})
// All the tiles are written at this point
// Only thing left to do is to create the index
const path = targetdir + "_" + layerId + "_overview.json"
const perX = {}
createdTiles.map(i => Utils.tile_from_index(i)).forEach(([z, x, y]) => {
const key = "" + x
if (perX[key] === undefined) {
perX[key] = []
}
perX[key].push(y)
})
writeFileSync(path, JSON.stringify(perX))
}
new PerLayerFeatureSourceSplitter(
new UIEventSource<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[]) {
@ -335,8 +287,8 @@ async function main(args: string[]) {
console.error("The theme " + theme + " was not found; try one of ", keys);
return
}
const overpass = createOverpassObject(theme)
const relationTracker = new RelationsTracker()
const overpass = createOverpassObject(theme, relationTracker)
let failed = 0;
do {
@ -348,21 +300,13 @@ async function main(args: string[]) {
} while (failed > 0)
const extraFeatures = await downloadExtraData(theme);
postProcess(targetdir, tileRange, theme, extraFeatures)
splitPerLayer(targetdir, tileRange, theme)
const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures)
postProcess(allFeaturesSource, theme, relationTracker, targetdir)
if (args[7] === "--generate-point-overview") {
const targetLayers = args[8].split(",")
for (const targetLayer of targetLayers) {
if (!theme.layers.some(l => l.id === targetLayer)) {
throw "Target layer " + targetLayer + " not found, did you mistype the name? Found layers are: " + theme.layers.map(l => l.id).join(",")
}
createOverview(targetdir, tileRange, zoomlevel, targetLayer)
}
}
}
let args = [...process.argv]
args.splice(0, 2)
main(args);
main(args);
console.log("All done!")