More refactoring, move minimap behind facade
This commit is contained in:
parent
c11ff652b8
commit
d5c1ba4cd1
79 changed files with 1848 additions and 1118 deletions
|
@ -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 ------------
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }[]>;
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }[]>;
|
||||
|
|
|
@ -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 }[]>([]);
|
||||
|
|
|
@ -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
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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".
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
23
State.ts
23
State.ts
|
@ -11,16 +11,14 @@ import InstalledThemes from "./Logic/Actors/InstalledThemes";
|
|||
import BaseLayer from "./Models/BaseLayer";
|
||||
import Loc from "./Models/Loc";
|
||||
import Constants from "./Models/Constants";
|
||||
|
||||
import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource";
|
||||
import TitleHandler from "./Logic/Actors/TitleHandler";
|
||||
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
|
||||
import {Relation} from "./Logic/Osm/ExtractRelations";
|
||||
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
|
||||
import OsmApiFeatureSource from "./Logic/FeatureSource/Sources/OsmApiFeatureSource";
|
||||
import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline";
|
||||
import FilteredLayer from "./Models/FilteredLayer";
|
||||
import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor";
|
||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
|
||||
import {BBox} from "./Logic/GeoOperations";
|
||||
|
||||
/**
|
||||
* Contains the global state: a bunch of UI-event sources
|
||||
|
@ -57,8 +55,6 @@ export default class State {
|
|||
|
||||
public favouriteLayers: UIEventSource<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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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())))
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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[]> }) {
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
|
28
Utils.ts
28
Utils.ts
|
@ -245,7 +245,6 @@ export class Utils {
|
|||
}
|
||||
dict.set(k, v());
|
||||
return dict.get(k);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -259,6 +258,26 @@ export class Utils {
|
|||
return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]]
|
||||
}
|
||||
|
||||
static tile_bounds_lon_lat(z: number, x: number, y: number): [[number, number], [number, number]] {
|
||||
return [[Utils.tile2long(x, z),Utils.tile2lat(y, z)], [Utils.tile2long(x + 1, z), Utils.tile2lat(y + 1, z)]]
|
||||
}
|
||||
|
||||
static tile_index(z: number, x: number, y: number):number{
|
||||
return ((x * (2 << z)) + y) * 100 + z
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a tile index number, returns [z, x, y]
|
||||
* @param index
|
||||
* @returns 'zxy'
|
||||
*/
|
||||
static tile_from_index(index: number) : [number, number, number]{
|
||||
const z = index % 100;
|
||||
const factor = 2 << z
|
||||
index = Math.floor(index / 100)
|
||||
return [z, Math.floor(index / factor), index % factor]
|
||||
}
|
||||
|
||||
/**
|
||||
* Return x, y of the tile containing (lat, lon) on the given zoom level
|
||||
*/
|
||||
|
@ -422,13 +441,6 @@ export class Utils {
|
|||
return bestColor ?? hex;
|
||||
}
|
||||
|
||||
public static setDefaults(options, defaults) {
|
||||
for (let key in defaults) {
|
||||
if (!(key in options)) options[key] = defaults[key];
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private static tile2long(x, z) {
|
||||
return (x / Math.pow(2, z) * 360 - 180);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -39,7 +39,7 @@
|
|||
"startLat": 0,
|
||||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 0.05,
|
||||
"widenFactor": 1,
|
||||
"roamingRenderings": [],
|
||||
"layers": [
|
||||
"public_bookcase"
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"defaultBackgroundId": "CartoDB.Voyager",
|
||||
"startLon": 4.351697,
|
||||
"startZoom": 16,
|
||||
"widenFactor": 0.05,
|
||||
"widenFactor": 2,
|
||||
"layers": [
|
||||
"drinking_water"
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -152,7 +152,7 @@
|
|||
]
|
||||
},
|
||||
"geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson",
|
||||
"geoJsonZoomLevel": 14,
|
||||
"geoJsonZoomLevel": 11,
|
||||
"isOsmCache": true
|
||||
},
|
||||
"title": {
|
||||
|
|
|
@ -131,7 +131,7 @@
|
|||
]
|
||||
},
|
||||
"then": {
|
||||
"en": "This object has no house number"
|
||||
"en": "This building has no house number"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
21
index.ts
21
index.ts
|
@ -8,31 +8,16 @@ import MoreScreen from "./UI/BigComponents/MoreScreen";
|
|||
import State from "./State";
|
||||
import Combine from "./UI/Base/Combine";
|
||||
import Translations from "./UI/i18n/Translations";
|
||||
|
||||
|
||||
import CountryCoder from "latlon2country"
|
||||
|
||||
import SimpleMetaTagger from "./Logic/SimpleMetaTagger";
|
||||
import Minimap from "./UI/Base/Minimap";
|
||||
import DirectionInput from "./UI/Input/DirectionInput";
|
||||
import SpecialVisualizations from "./UI/SpecialVisualizations";
|
||||
import ShowDataLayer from "./UI/ShowDataLayer";
|
||||
import * as L from "leaflet";
|
||||
import ValidatedTextField from "./UI/Input/ValidatedTextField";
|
||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig";
|
||||
import Constants from "./Models/Constants";
|
||||
import MinimapImplementation from "./UI/Base/MinimapImplementation";
|
||||
|
||||
MinimapImplementation.initialize()
|
||||
// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts
|
||||
SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/");
|
||||
DirectionInput.constructMinimap = options => new Minimap(options)
|
||||
ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref)
|
||||
SpecialVisualizations.constructMiniMap = options => new Minimap(options)
|
||||
SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
layoutToUse: UIEventSource<LayoutConfig>,
|
||||
enablePopups = true,
|
||||
zoomToFeatures = false) => new ShowDataLayer(features, leafletMap, layoutToUse, enablePopups, zoomToFeatures)
|
||||
|
||||
|
||||
let defaultLayout = ""
|
||||
// --------------------- Special actions based on the parameters -----------------
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1351,6 +1351,11 @@
|
|||
"title": {
|
||||
"render": "Known address"
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"title": {
|
||||
"render": "{name}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortDescription": "Help to build an open dataset of UK addresses",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -7,7 +7,6 @@ import {LayerConfigJson} from "../Models/ThemeConfig/Json/LayerConfigJson";
|
|||
|
||||
Utils.runningFromConsole = true
|
||||
|
||||
|
||||
export default class ScriptUtils {
|
||||
|
||||
|
||||
|
|
|
@ -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!")
|
Loading…
Reference in a new issue