forked from MapComplete/MapComplete
Merge files from develop branch
This commit is contained in:
parent
2a03b699d5
commit
3f29632cf4
16 changed files with 484 additions and 135 deletions
|
@ -185,6 +185,9 @@ export class InitUiElements {
|
|||
// Reset the loading message once things are loaded
|
||||
new CenterMessageBox().AttachTo("centermessage");
|
||||
|
||||
// At last, zoom to the needed location if the focus is on an element
|
||||
|
||||
|
||||
}
|
||||
|
||||
static LoadLayoutFromHash(userLayoutParam: UIEventSource<string>) {
|
||||
|
@ -391,12 +394,21 @@ export class InitUiElements {
|
|||
|
||||
const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap);
|
||||
State.state.layerUpdater = updater;
|
||||
const source = new FeaturePipeline(state.filteredLayers, updater, state.layoutToUse, state.changes, state.locationControl);
|
||||
|
||||
|
||||
|
||||
const source = new FeaturePipeline(state.filteredLayers,
|
||||
updater,
|
||||
state.osmApiFeatureSource,
|
||||
state.layoutToUse,
|
||||
state.changes,
|
||||
state.locationControl,
|
||||
state.selectedElement);
|
||||
|
||||
new ShowDataLayer(source.features, State.state.leafletMap, State.state.layoutToUse);
|
||||
|
||||
new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source);
|
||||
|
||||
const selectedFeatureHandler = new SelectedFeatureHandler(Hash.hash, State.state.selectedElement, source, State.state.osmApiFeatureSource);
|
||||
selectedFeatureHandler.zoomToSelectedFeature(State.state.locationControl);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import FeatureSource from "../FeatureSource/FeatureSource";
|
||||
import {OsmObject, OsmObjectMeta} from "../Osm/OsmObject";
|
||||
import Loc from "../../Models/Loc";
|
||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
|
||||
import OsmApiFeatureSource from "../FeatureSource/OsmApiFeatureSource";
|
||||
|
||||
/**
|
||||
* Makes sure the hash shows the selected element and vice-versa
|
||||
* Makes sure the hash shows the selected element and vice-versa.
|
||||
*/
|
||||
export default class SelectedFeatureHandler {
|
||||
private readonly _featureSource: FeatureSource;
|
||||
|
@ -10,13 +14,16 @@ export default class SelectedFeatureHandler {
|
|||
private readonly _selectedFeature: UIEventSource<any>;
|
||||
|
||||
private static readonly _no_trigger_on = ["welcome","copyright","layers"]
|
||||
private readonly _osmApiSource: OsmApiFeatureSource;
|
||||
|
||||
constructor(hash: UIEventSource<string>,
|
||||
selectedFeature: UIEventSource<any>,
|
||||
featureSource: FeatureSource) {
|
||||
featureSource: FeaturePipeline,
|
||||
osmApiSource: OsmApiFeatureSource) {
|
||||
this._hash = hash;
|
||||
this._selectedFeature = selectedFeature;
|
||||
this._featureSource = featureSource;
|
||||
this._osmApiSource = osmApiSource;
|
||||
const self = this;
|
||||
hash.addCallback(h => {
|
||||
if (h === undefined || h === "") {
|
||||
|
@ -26,6 +33,8 @@ export default class SelectedFeatureHandler {
|
|||
}
|
||||
})
|
||||
|
||||
hash.addCallbackAndRun(h => self.downloadFeature(h))
|
||||
|
||||
featureSource.features.addCallback(_ => self.selectFeature());
|
||||
|
||||
selectedFeature.addCallback(feature => {
|
||||
|
@ -45,6 +54,32 @@ export default class SelectedFeatureHandler {
|
|||
|
||||
}
|
||||
|
||||
// If a feature is selected via the hash, zoom there
|
||||
public zoomToSelectedFeature(location: UIEventSource<Loc>){
|
||||
const hash = this._hash.data;
|
||||
if(hash === undefined || SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){
|
||||
return; // No valid feature selected
|
||||
}
|
||||
// We should have a valid osm-ID and zoom to it
|
||||
OsmObject.DownloadObject(hash, (element: OsmObject, meta: OsmObjectMeta) => {
|
||||
const centerpoint = element.centerpoint();
|
||||
console.log("Zooming to location for select point: ", centerpoint)
|
||||
location.data.lat = centerpoint[0]
|
||||
location.data.lon = centerpoint[1]
|
||||
location.ping();
|
||||
})
|
||||
}
|
||||
|
||||
private downloadFeature(hash: string){
|
||||
if(hash === undefined || hash === ""){
|
||||
return;
|
||||
}
|
||||
if(SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){
|
||||
return;
|
||||
}
|
||||
this._osmApiSource.load(hash)
|
||||
}
|
||||
|
||||
private selectFeature(){
|
||||
const features = this._featureSource?.features?.data;
|
||||
if(features === undefined){
|
||||
|
|
|
@ -48,6 +48,7 @@ export class ElementStorage {
|
|||
const keptKeys = es.data;
|
||||
// The element already exists
|
||||
// We use the new feature to overwrite all the properties in the already existing eventsource
|
||||
console.log("Merging multiple instances of ", elementId)
|
||||
let somethingChanged = false;
|
||||
for (const k in newProperties) {
|
||||
const v = newProperties[k];
|
||||
|
|
|
@ -18,27 +18,32 @@ export default class FeaturePipeline implements FeatureSource {
|
|||
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
|
||||
public readonly name = "FeaturePipeline"
|
||||
|
||||
public readonly name = "FeaturePipeline"
|
||||
|
||||
constructor(flayers: UIEventSource<{ isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[]>,
|
||||
updater: FeatureSource,
|
||||
fromOsmApi: FeatureSource,
|
||||
layout: UIEventSource<LayoutConfig>,
|
||||
newPoints: FeatureSource,
|
||||
locationControl: UIEventSource<Loc>) {
|
||||
locationControl: UIEventSource<Loc>,
|
||||
selectedElement: UIEventSource<any>) {
|
||||
|
||||
// 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 amendedOverpassSource =
|
||||
new RememberingSource(
|
||||
new LocalStorageSaver(
|
||||
new MetaTaggingFeatureSource( // first we metatag, then we save to get the metatags into storage too
|
||||
new RegisteringFeatureSource(
|
||||
new FeatureDuplicatorPerLayer(flayers,
|
||||
new FeatureDuplicatorPerLayer(flayers,
|
||||
new MetaTaggingFeatureSource(
|
||||
new RegisteringFeatureSource(
|
||||
updater)
|
||||
)), layout));
|
||||
|
||||
const geojsonSources: FeatureSource [] = GeoJsonSource
|
||||
.ConstructMultiSource(flayers.data, locationControl)
|
||||
.map(geojsonSource => new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource)));
|
||||
|
||||
.map(geojsonSource => new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource)));
|
||||
|
||||
const amendedLocalStorageSource =
|
||||
new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout))
|
||||
));
|
||||
|
@ -46,10 +51,16 @@ export default class FeaturePipeline implements FeatureSource {
|
|||
newPoints = new MetaTaggingFeatureSource(new FeatureDuplicatorPerLayer(flayers,
|
||||
new RegisteringFeatureSource(newPoints)));
|
||||
|
||||
const amendedOsmApiSource = new RememberingSource(
|
||||
new FeatureDuplicatorPerLayer(flayers,
|
||||
new MetaTaggingFeatureSource(
|
||||
new RegisteringFeatureSource(fromOsmApi))));
|
||||
|
||||
const merged =
|
||||
|
||||
new FeatureSourceMerger([
|
||||
amendedOverpassSource,
|
||||
amendedOsmApiSource,
|
||||
amendedLocalStorageSource,
|
||||
newPoints,
|
||||
...geojsonSources
|
||||
|
@ -60,6 +71,7 @@ export default class FeaturePipeline implements FeatureSource {
|
|||
new FilteringFeatureSource(
|
||||
flayers,
|
||||
locationControl,
|
||||
selectedElement,
|
||||
merged
|
||||
));
|
||||
this.features = source.features;
|
||||
|
|
|
@ -21,27 +21,48 @@ export default class FeatureSourceMerger implements FeatureSource {
|
|||
}
|
||||
|
||||
private Update() {
|
||||
let all = {}; // Mapping 'id' -> {feature, freshness}
|
||||
|
||||
let somethingChanged = false;
|
||||
const all: Map<string, { feature: any, freshness: Date }> = new Map<string, { feature: any; freshness: Date }>();
|
||||
// We seed the dictionary with the previously loaded features
|
||||
const oldValues = this.features.data ?? [];
|
||||
for (const oldValue of oldValues) {
|
||||
all.set(oldValue.feature.id, oldValue)
|
||||
}
|
||||
|
||||
for (const source of this._sources) {
|
||||
if (source?.features?.data === undefined) {
|
||||
continue;
|
||||
}
|
||||
for (const f of source.features.data) {
|
||||
const id = f.feature.properties.id;
|
||||
const oldV = all[id];
|
||||
if (oldV === undefined) {
|
||||
all[id] = f;
|
||||
} else {
|
||||
if (oldV.freshness < f.freshness) {
|
||||
all[id] = f;
|
||||
}
|
||||
if (!all.has(id)) {
|
||||
// This is a new feature
|
||||
somethingChanged = true;
|
||||
all.set(id, f);
|
||||
continue;
|
||||
}
|
||||
|
||||
// This value has been seen already, either in a previous run or by a previous datasource
|
||||
// Let's figure out if something changed
|
||||
const oldV = all.get(id);
|
||||
if (oldV.freshness < f.freshness) {
|
||||
// Jup, this feature is fresher
|
||||
all.set(id, f);
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const newList = [];
|
||||
for (const id in all) {
|
||||
newList.push(all[id]);
|
||||
|
||||
if(!somethingChanged){
|
||||
// We don't bother triggering an update
|
||||
return;
|
||||
}
|
||||
|
||||
const newList = [];
|
||||
all.forEach((value, key) => {
|
||||
newList.push(value)
|
||||
})
|
||||
this.features.setData(newList);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,14 @@ import Loc from "../../Models/Loc";
|
|||
|
||||
export default class FilteringFeatureSource implements FeatureSource {
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name = "FilteringFeatureSource"
|
||||
public readonly name = "FilteringFeatureSource"
|
||||
|
||||
constructor(layers: UIEventSource<{
|
||||
isDisplayed: UIEventSource<boolean>,
|
||||
layerDef: LayerConfig
|
||||
}[]>,
|
||||
location: UIEventSource<Loc>,
|
||||
selectedElement: UIEventSource<any>,
|
||||
upstream: FeatureSource) {
|
||||
|
||||
const self = this;
|
||||
|
@ -18,7 +20,7 @@ public readonly name = "FilteringFeatureSource"
|
|||
function update() {
|
||||
|
||||
const layerDict = {};
|
||||
if(layers.data.length == 0){
|
||||
if (layers.data.length == 0) {
|
||||
throw "No layers defined!"
|
||||
}
|
||||
for (const layer of layers.data) {
|
||||
|
@ -32,6 +34,11 @@ public readonly name = "FilteringFeatureSource"
|
|||
const newFeatures = features.filter(f => {
|
||||
const layerId = f.feature._matching_layer_id;
|
||||
|
||||
if(selectedElement.data !== undefined && selectedElement.data?.id === f.feature.id){
|
||||
// This is the selected object - it gets a free pass even if zoom is not sufficient
|
||||
return true;
|
||||
}
|
||||
|
||||
if (layerId !== undefined) {
|
||||
const layer: {
|
||||
isDisplayed: UIEventSource<boolean>,
|
||||
|
@ -41,16 +48,16 @@ public readonly name = "FilteringFeatureSource"
|
|||
missingLayers.add(layerId)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const isShown = layer.layerDef.isShown
|
||||
const tags = f.feature.properties;
|
||||
if(isShown.IsKnown(tags)){
|
||||
if (isShown.IsKnown(tags)) {
|
||||
const result = layer.layerDef.isShown.GetRenderValue(f.feature.properties).txt;
|
||||
if(result !== "yes"){
|
||||
if (result !== "yes") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (FilteringFeatureSource.showLayer(layer, location)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -69,7 +76,7 @@ public readonly name = "FilteringFeatureSource"
|
|||
});
|
||||
console.log("Filtering layer source: input: ", upstream.features.data?.length, "output:", newFeatures.length)
|
||||
self.features.setData(newFeatures);
|
||||
if(missingLayers.size > 0){
|
||||
if (missingLayers.size > 0) {
|
||||
console.error("Some layers were not found: ", Array.from(missingLayers))
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +93,7 @@ public readonly name = "FilteringFeatureSource"
|
|||
if (l.zoom < layer.layerDef.minzoom) {
|
||||
continue;
|
||||
}
|
||||
if(l.zoom > layer.layerDef.maxzoom){
|
||||
if (l.zoom > layer.layerDef.maxzoom) {
|
||||
continue;
|
||||
}
|
||||
if (!layer.isDisplayed.data) {
|
||||
|
@ -98,13 +105,13 @@ public readonly name = "FilteringFeatureSource"
|
|||
}).addCallback(() => {
|
||||
update();
|
||||
});
|
||||
|
||||
|
||||
layers.addCallback(update);
|
||||
|
||||
|
||||
const registered = new Set<UIEventSource<boolean>>();
|
||||
layers.addCallbackAndRun(layers => {
|
||||
for (const layer of layers) {
|
||||
if(registered.has(layer.isDisplayed)){
|
||||
if (registered.has(layer.isDisplayed)) {
|
||||
continue;
|
||||
}
|
||||
registered.add(layer.isDisplayed);
|
||||
|
|
|
@ -20,9 +20,9 @@ export default class GeoJsonSource implements FeatureSource {
|
|||
private readonly layerId: string;
|
||||
private readonly seenids: Set<string> = new Set<string>()
|
||||
|
||||
private constructor(locationControl: UIEventSource<Loc>,
|
||||
flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig },
|
||||
onFail?: ((errorMsg: any) => void)) {
|
||||
private constructor(locationControl: UIEventSource<Loc>,
|
||||
flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig },
|
||||
onFail?: ((errorMsg: any) => void)) {
|
||||
this.layerId = flayer.layerDef.id;
|
||||
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
|
||||
this.name = "GeoJsonSource of " + url;
|
||||
|
@ -33,7 +33,7 @@ export default class GeoJsonSource implements FeatureSource {
|
|||
if (zoomLevel === undefined) {
|
||||
// This is a classic, static geojson layer
|
||||
if (onFail === undefined) {
|
||||
onFail = errorMsg => {
|
||||
onFail = _ => {
|
||||
}
|
||||
}
|
||||
this.onFail = onFail;
|
||||
|
@ -43,57 +43,6 @@ export default class GeoJsonSource implements FeatureSource {
|
|||
this.ConfigureDynamicLayer(url, zoomLevel, locationControl, flayer)
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigureDynamicLayer(url: string, zoomLevel: number, locationControl: UIEventSource<Loc>, flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }){
|
||||
// This is a dynamic template with a fixed zoom level
|
||||
url = url.replace("{z}", "" + zoomLevel)
|
||||
const loadedTiles = new Set<string>();
|
||||
const self = this;
|
||||
this.onFail = (msg, url) => {
|
||||
console.warn(`Could not load geojson layer from`, url, "due to", msg)
|
||||
loadedTiles.add(url); // We add the url to the 'loadedTiles' in order to not reload it in the future
|
||||
}
|
||||
|
||||
const neededTiles = locationControl.map(
|
||||
location => {
|
||||
// Yup, this is cheating to just get the bounds here
|
||||
const bounds = State.state.leafletMap.data.getBounds()
|
||||
const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
|
||||
const needed = new Set<string>();
|
||||
for (let x = tileRange.xstart; x <= tileRange.xend; x++) {
|
||||
for (let y = tileRange.ystart; y <= tileRange.yend; y++) {
|
||||
let neededUrl = url.replace("{x}", "" + x).replace("{y}", "" + y);
|
||||
needed.add(neededUrl)
|
||||
}
|
||||
}
|
||||
return needed;
|
||||
}
|
||||
, [flayer.isDisplayed]);
|
||||
neededTiles.stabilized(250).addCallback((needed: Set<string>) => {
|
||||
if (needed === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!flayer.isDisplayed.data) {
|
||||
// No need to download! - the layer is disabled
|
||||
return;
|
||||
}
|
||||
|
||||
if(locationControl.data.zoom < flayer.layerDef.minzoom){
|
||||
return;
|
||||
}
|
||||
|
||||
needed.forEach(neededTile => {
|
||||
if (loadedTiles.has(neededTile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadedTiles.add(neededTile)
|
||||
self.LoadJSONFrom(neededTile)
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges together the layers which have the same source
|
||||
|
@ -152,6 +101,53 @@ export default class GeoJsonSource implements FeatureSource {
|
|||
|
||||
}
|
||||
|
||||
private ConfigureDynamicLayer(url: string, zoomLevel: number, locationControl: UIEventSource<Loc>, flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }) {
|
||||
// This is a dynamic template with a fixed zoom level
|
||||
url = url.replace("{z}", "" + zoomLevel)
|
||||
const loadedTiles = new Set<string>();
|
||||
const self = this;
|
||||
this.onFail = (msg, url) => {
|
||||
console.warn(`Could not load geojson layer from`, url, "due to", msg)
|
||||
loadedTiles.add(url); // We add the url to the 'loadedTiles' in order to not reload it in the future
|
||||
}
|
||||
|
||||
const neededTiles = locationControl.map(
|
||||
_ => {
|
||||
// Yup, this is cheating to just get the bounds here
|
||||
const bounds = State.state.leafletMap.data.getBounds()
|
||||
const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
|
||||
const needed = Utils.MapRange(tileRange, (x, y) => {
|
||||
return url.replace("{x}", "" + x).replace("{y}", "" + y);
|
||||
})
|
||||
return new Set<string>(needed);
|
||||
}
|
||||
, [flayer.isDisplayed]);
|
||||
neededTiles.stabilized(250).addCallback((needed: Set<string>) => {
|
||||
if (needed === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!flayer.isDisplayed.data) {
|
||||
// No need to download! - the layer is disabled
|
||||
return;
|
||||
}
|
||||
|
||||
if (locationControl.data.zoom < flayer.layerDef.minzoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
needed.forEach(neededTile => {
|
||||
if (loadedTiles.has(neededTile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadedTiles.add(neededTile)
|
||||
self.LoadJSONFrom(neededTile)
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private LoadJSONFrom(url: string) {
|
||||
const eventSource = this.features;
|
||||
const self = this;
|
||||
|
|
91
Logic/FeatureSource/OsmApiFeatureSource.ts
Normal file
91
Logic/FeatureSource/OsmApiFeatureSource.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
import FeatureSource from "./FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {OsmObject} from "../Osm/OsmObject";
|
||||
import State from "../../State";
|
||||
import {Utils} from "../../Utils";
|
||||
import Loc from "../../Models/Loc";
|
||||
|
||||
|
||||
export default class OsmApiFeatureSource implements FeatureSource {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string = "OsmApiFeatureSource";
|
||||
private readonly loadedTiles: Set<string> = new Set<string>();
|
||||
|
||||
constructor(location: UIEventSource<Loc>) {
|
||||
/* const self = this
|
||||
location.addCallback(_ => {
|
||||
self.loadArea()
|
||||
})
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
public load(id: string) {
|
||||
OsmObject.DownloadObject(id, (element, meta) => {
|
||||
const geojson = element.asGeoJson();
|
||||
geojson.id = geojson.properties.id;
|
||||
this.features.setData([{feature: geojson, freshness: meta["_last_edit:timestamp"]}])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the current inview-area
|
||||
*/
|
||||
public loadArea(z: number = 16): boolean {
|
||||
const layers = State.state.filteredLayers.data;
|
||||
|
||||
const disabledLayers = layers.filter(layer => layer.layerDef.source.overpassScript !== undefined || layer.layerDef.source.geojsonSource !== undefined)
|
||||
if (disabledLayers.length > 0) {
|
||||
return false;
|
||||
}
|
||||
const loc = State.state.locationControl.data;
|
||||
if (loc.zoom < 16) {
|
||||
return false;
|
||||
}
|
||||
if (State.state.leafletMap.data === undefined) {
|
||||
return false; // Not yet inited
|
||||
}
|
||||
const bounds = State.state.leafletMap.data.getBounds()
|
||||
const tileRange = Utils.TileRangeBetween(z, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
|
||||
const self = this;
|
||||
Utils.MapRange(tileRange, (x, y) => {
|
||||
const key = x + "/" + y;
|
||||
if (self.loadedTiles.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.loadedTiles.add(key);
|
||||
|
||||
const bounds = Utils.tile_bounds(z, x, y);
|
||||
console.log("Loading OSM data tile", z, x, y, " with bounds", bounds)
|
||||
OsmObject.LoadArea(bounds, objects => {
|
||||
const keptGeoJson: {feature:any, freshness: Date}[] = []
|
||||
// Which layer does the object match?
|
||||
for (const object of objects) {
|
||||
|
||||
for (const flayer of layers) {
|
||||
const layer = flayer.layerDef;
|
||||
const tags = object.tags
|
||||
const doesMatch = layer.source.osmTags.matchesProperties(tags);
|
||||
if (doesMatch) {
|
||||
const geoJson = object.asGeoJson();
|
||||
geoJson._matching_layer_id = layer.id
|
||||
keptGeoJson.push({feature: geoJson, freshness: object.timestamp})
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
self.features.setData(keptGeoJson)
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
import LayerConfig from "../Customizations/JSON/LayerConfig";
|
||||
import SimpleMetaTagger from "./SimpleMetaTagger";
|
||||
import {ExtraFunction} from "./ExtraFunction";
|
||||
import State from "../State";
|
||||
import {Relation} from "./Osm/ExtractRelations";
|
||||
import {meta} from "@turf/turf";
|
||||
|
||||
|
||||
interface Params {
|
||||
|
|
|
@ -9,10 +9,14 @@ export abstract class OsmObject {
|
|||
tags: {} = {};
|
||||
version: number;
|
||||
public changed: boolean = false;
|
||||
timestamp: Date;
|
||||
|
||||
protected constructor(type: string, id: number) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.tags = {
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
static DownloadObject(id, continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
|
||||
|
@ -20,8 +24,7 @@ export abstract class OsmObject {
|
|||
const type = splitted[0];
|
||||
const idN = splitted[1];
|
||||
|
||||
const newContinuation = (element: OsmObject, meta :OsmObjectMeta) => {
|
||||
console.log("Received: ", element, "with meta", meta);
|
||||
const newContinuation = (element: OsmObject, meta: OsmObjectMeta) => {
|
||||
continuation(element, meta);
|
||||
}
|
||||
|
||||
|
@ -36,6 +39,80 @@ export abstract class OsmObject {
|
|||
}
|
||||
}
|
||||
|
||||
public static DownloadHistory(id: string, continuation: (versions: OsmObject[]) => void): void {
|
||||
const splitted = id.split("/");
|
||||
const type = splitted[0];
|
||||
const idN = splitted[1];
|
||||
$.getJSON("https://openStreetMap.org/api/0.6/" + type + "/" + idN + "/history", data => {
|
||||
const elements: any[] = data.elements;
|
||||
const osmObjects: OsmObject[] = []
|
||||
for (const element of elements) {
|
||||
let osmObject: OsmObject = null
|
||||
switch (type) {
|
||||
case("node"):
|
||||
osmObject = new OsmNode(idN);
|
||||
break;
|
||||
case("way"):
|
||||
osmObject = new OsmWay(idN);
|
||||
break;
|
||||
case("relation"):
|
||||
osmObject = new OsmRelation(idN);
|
||||
break;
|
||||
}
|
||||
osmObject?.LoadData(element);
|
||||
osmObject?.SaveExtraData(element, []);
|
||||
osmObjects.push(osmObject)
|
||||
}
|
||||
continuation(osmObjects)
|
||||
})
|
||||
}
|
||||
|
||||
private static ParseObjects(elements: any[]) : OsmObject[]{
|
||||
const objects: OsmObject[] = [];
|
||||
const allNodes: Map<number, OsmNode> = new Map<number, OsmNode>()
|
||||
for (const element of elements) {
|
||||
const type = element.type;
|
||||
const idN = element.id;
|
||||
let osmObject: OsmObject = null
|
||||
switch (type) {
|
||||
case("node"):
|
||||
const node = new OsmNode(idN);
|
||||
allNodes.set(idN, node);
|
||||
osmObject = node
|
||||
node.SaveExtraData(element);
|
||||
break;
|
||||
case("way"):
|
||||
osmObject = new OsmWay(idN);
|
||||
const nodes = element.nodes.map(i => allNodes.get(i));
|
||||
osmObject.SaveExtraData(element, nodes)
|
||||
break;
|
||||
case("relation"):
|
||||
osmObject = new OsmRelation(idN);
|
||||
osmObject.SaveExtraData(element, [])
|
||||
break;
|
||||
}
|
||||
osmObject.LoadData(element)
|
||||
objects.push(osmObject)
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
//Loads an area from the OSM-api.
|
||||
// bounds should be: [[maxlat, minlon], [minlat, maxlon]] (same as Utils.tile_bounds)
|
||||
public static LoadArea(bounds: [[number, number], [number, number]], callback: (objects: OsmObject[]) => void) {
|
||||
const minlon = bounds[0][1]
|
||||
const maxlon = bounds[1][1]
|
||||
const minlat = bounds[1][0]
|
||||
const maxlat = bounds[0][0];
|
||||
const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
|
||||
$.getJSON(url, data => {
|
||||
const elements: any[] = data.elements;
|
||||
const objects = OsmObject.ParseObjects(elements)
|
||||
callback(objects);
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
public static DownloadAll(neededIds, knownElements: any = {}, continuation: ((knownObjects: any) => void)) {
|
||||
// local function which downloads all the objects one by one
|
||||
// this is one big loop, running one download, then rerunning the entire function
|
||||
|
@ -50,7 +127,6 @@ export abstract class OsmObject {
|
|||
return;
|
||||
}
|
||||
|
||||
console.log("Downloading ", neededId);
|
||||
OsmObject.DownloadObject(neededId,
|
||||
function (element) {
|
||||
knownElements[neededId] = element; // assign the element for later, continue downloading the next element
|
||||
|
@ -59,7 +135,12 @@ export abstract class OsmObject {
|
|||
);
|
||||
}
|
||||
|
||||
abstract SaveExtraData(element);
|
||||
// The centerpoint of the feature, as [lat, lon]
|
||||
public abstract centerpoint(): [number, number];
|
||||
|
||||
public abstract asGeoJson(): any;
|
||||
|
||||
abstract SaveExtraData(element: any, allElements: any[]);
|
||||
|
||||
/**
|
||||
* Generates the changeset-XML for tags
|
||||
|
@ -78,12 +159,20 @@ export abstract class OsmObject {
|
|||
|
||||
Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
|
||||
const self = this;
|
||||
$.getJSON("https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id,
|
||||
function (data) {
|
||||
const element = data.elements[0];
|
||||
self.tags = element.tags;
|
||||
self.version = element.version;
|
||||
self.SaveExtraData(element);
|
||||
const full = this.type !== "way" ? "" : "/full";
|
||||
const url = "https://www.openstreetmap.org/api/0.6/" + this.type + "/" + this.id + full;
|
||||
$.getJSON(url, function (data) {
|
||||
|
||||
const element = data.elements.pop();
|
||||
|
||||
let nodes = []
|
||||
if(data.elements.length > 2){
|
||||
nodes = OsmObject.ParseObjects(data.elements)
|
||||
}
|
||||
|
||||
self.LoadData(element)
|
||||
self.SaveExtraData(element, nodes);
|
||||
|
||||
continuation(self, {
|
||||
"_last_edit:contributor": element.user,
|
||||
"_last_edit:contributor:uid": element.uid,
|
||||
|
@ -102,7 +191,7 @@ export abstract class OsmObject {
|
|||
if (oldV == v) {
|
||||
return;
|
||||
}
|
||||
console.log("WARNING: overwriting ", oldV, " with ", v, " for key ", k)
|
||||
console.log("Overwriting ", oldV, " with ", v, " for key ", k)
|
||||
}
|
||||
this.tags[k] = v;
|
||||
if (v === undefined || v === "") {
|
||||
|
@ -119,6 +208,25 @@ export abstract class OsmObject {
|
|||
}
|
||||
return 'version="' + this.version + '"';
|
||||
}
|
||||
|
||||
private LoadData(element: any): void {
|
||||
this.tags = element.tags ?? this.tags;
|
||||
this.version = element.version;
|
||||
this.timestamp = element.timestamp;
|
||||
const tgs = this.tags;
|
||||
if(element.tags === undefined){
|
||||
// Simple node which is part of a way - not important
|
||||
return;
|
||||
}
|
||||
tgs["_last_edit:contributor"] = element.user
|
||||
tgs["_last_edit:contributor:uid"] = element.uid
|
||||
tgs["_last_edit:changeset"] = element.changeset
|
||||
tgs["_last_edit:timestamp"] = element.timestamp
|
||||
tgs["_version_number"] = element.version
|
||||
tgs["id"] = this.type + "/" + this.id;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -135,21 +243,36 @@ export class OsmNode extends OsmObject {
|
|||
ChangesetXML(changesetId: string): string {
|
||||
let tags = this.TagsXML();
|
||||
|
||||
let change =
|
||||
' <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' +
|
||||
return ' <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' +
|
||||
tags +
|
||||
' </node>\n';
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
SaveExtraData(element) {
|
||||
this.lat = element.lat;
|
||||
this.lon = element.lon;
|
||||
}
|
||||
|
||||
centerpoint(): [number, number] {
|
||||
return [this.lat, this.lon];
|
||||
}
|
||||
|
||||
asGeoJson() {
|
||||
return {
|
||||
"type": "Feature",
|
||||
"properties": this.tags,
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [
|
||||
this.lon,
|
||||
this.lat
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface OsmObjectMeta{
|
||||
export interface OsmObjectMeta {
|
||||
"_last_edit:contributor": string,
|
||||
"_last_edit:contributor:uid": number,
|
||||
"_last_edit:changeset": number,
|
||||
|
@ -161,12 +284,19 @@ export interface OsmObjectMeta{
|
|||
export class OsmWay extends OsmObject {
|
||||
|
||||
nodes: number[];
|
||||
coordinates: [number, number][] = []
|
||||
lat: number;
|
||||
lon: number;
|
||||
|
||||
constructor(id) {
|
||||
super("way", id);
|
||||
|
||||
}
|
||||
|
||||
centerpoint(): [number, number] {
|
||||
return [this.lat, this.lon];
|
||||
}
|
||||
|
||||
ChangesetXML(changesetId: string): string {
|
||||
let tags = this.TagsXML();
|
||||
let nds = "";
|
||||
|
@ -174,18 +304,39 @@ export class OsmWay extends OsmObject {
|
|||
nds += ' <nd ref="' + this.nodes[node] + '"/>\n';
|
||||
}
|
||||
|
||||
let change =
|
||||
' <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
|
||||
return ' <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
|
||||
nds +
|
||||
tags +
|
||||
' </way>\n';
|
||||
|
||||
return change;
|
||||
}
|
||||
|
||||
SaveExtraData(element) {
|
||||
SaveExtraData(element, allNodes: OsmNode[]) {
|
||||
|
||||
let latSum = 0
|
||||
let lonSum = 0
|
||||
|
||||
for (const node of allNodes) {
|
||||
const cp = node.centerpoint();
|
||||
this.coordinates.push(cp);
|
||||
latSum = cp[0]
|
||||
lonSum = cp[1]
|
||||
}
|
||||
let count = this.coordinates.length;
|
||||
this.lat = latSum / count;
|
||||
this.lon = lonSum / count;
|
||||
this.nodes = element.nodes;
|
||||
}
|
||||
|
||||
asGeoJson() {
|
||||
return {
|
||||
"type": "Feature",
|
||||
"properties": this.tags,
|
||||
"geometry": {
|
||||
"type": "LineString",
|
||||
"coordinates": this.coordinates.map(c => [c[1], c[0]])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OsmRelation extends OsmObject {
|
||||
|
@ -197,24 +348,29 @@ export class OsmRelation extends OsmObject {
|
|||
|
||||
}
|
||||
|
||||
centerpoint(): [number, number] {
|
||||
return [0, 0]; // TODO
|
||||
}
|
||||
|
||||
ChangesetXML(changesetId: string): string {
|
||||
let members = "";
|
||||
for (const memberI in this.members) {
|
||||
const member = this.members[memberI];
|
||||
for (const member of this.members) {
|
||||
members += ' <member type="' + member.type + '" ref="' + member.ref + '" role="' + member.role + '"/>\n';
|
||||
}
|
||||
|
||||
let tags = this.TagsXML();
|
||||
let change =
|
||||
' <relation id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
|
||||
return ' <relation id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' +
|
||||
members +
|
||||
tags +
|
||||
' </relation>\n';
|
||||
return change;
|
||||
|
||||
}
|
||||
|
||||
SaveExtraData(element) {
|
||||
this.members = element.members;
|
||||
}
|
||||
|
||||
asGeoJson() {
|
||||
throw "Not Implemented"
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import {Utils} from "../Utils";
|
|||
import opening_hours from "opening_hours";
|
||||
import {UIElement} from "../UI/UIElement";
|
||||
import Combine from "../UI/Base/Combine";
|
||||
import UpdateTagsFromOsmAPI from "./Actors/UpdateTagsFromOsmAPI";
|
||||
|
||||
|
||||
const cardinalDirections = {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Utils } from "../Utils";
|
|||
|
||||
export default class Constants {
|
||||
|
||||
public static vNumber = "0.7.0c";
|
||||
public static vNumber = "0.7.1b";
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
|
|
7
State.ts
7
State.ts
|
@ -18,7 +18,7 @@ import LayerConfig from "./Customizations/JSON/LayerConfig";
|
|||
import TitleHandler from "./Logic/Actors/TitleHandler";
|
||||
import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader";
|
||||
import {Relation} from "./Logic/Osm/ExtractRelations";
|
||||
import UpdateTagsFromOsmAPI from "./Logic/Actors/UpdateTagsFromOsmAPI";
|
||||
import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource";
|
||||
|
||||
/**
|
||||
* Contains the global state: a bunch of UI-event sources
|
||||
|
@ -58,6 +58,8 @@ export default class State {
|
|||
public favouriteLayers: UIEventSource<string[]>;
|
||||
|
||||
public layerUpdater: UpdateFromOverpass;
|
||||
|
||||
public osmApiFeatureSource : OsmApiFeatureSource ;
|
||||
|
||||
|
||||
public filteredLayers: UIEventSource<{
|
||||
|
@ -218,6 +220,7 @@ export default class State {
|
|||
|
||||
this.allElements = new ElementStorage();
|
||||
this.changes = new Changes();
|
||||
this.osmApiFeatureSource = new OsmApiFeatureSource(this.locationControl)
|
||||
|
||||
new PendingChangesUploader(this.changes, this.selectedElement);
|
||||
|
||||
|
@ -253,8 +256,6 @@ export default class State {
|
|||
|
||||
new TitleHandler(this.layoutToUse, this.selectedElement, this.allElements);
|
||||
|
||||
new UpdateTagsFromOsmAPI(this.selectedElement.map(el => el?.properties?.id), this.allElements)
|
||||
|
||||
}
|
||||
|
||||
private static asFloat(source: UIEventSource<string>): UIEventSource<number> {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import {VerticalCombine} from "../Base/VerticalCombine";
|
||||
import {UIElement} from "../UIElement";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
|
@ -53,15 +52,17 @@ export default class MoreScreen extends UIElement {
|
|||
if(path === ""){
|
||||
path = "."
|
||||
}
|
||||
|
||||
const params = `z=${currentLocation.zoom ?? 1}&lat=${currentLocation.lat ?? 0}&lon=${currentLocation.lon ?? 0}`
|
||||
let linkText =
|
||||
`${path}/${layout.id.toLowerCase()}?z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
|
||||
`${path}/${layout.id.toLowerCase()}?${params}`
|
||||
|
||||
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
|
||||
linkText = `${path}/index.html?layout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}`
|
||||
linkText = `${path}/index.html?layout=${layout.id}&${params}`
|
||||
}
|
||||
|
||||
if (customThemeDefinition) {
|
||||
linkText = `${path}/?userlayout=${layout.id}&z=${currentLocation.zoom}&lat=${currentLocation.lat}&lon=${currentLocation.lon}#${customThemeDefinition}`
|
||||
linkText = `${path}/?userlayout=${layout.id}&${params}#${customThemeDefinition}`
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -162,9 +162,8 @@ export default class ShowDataLayer {
|
|||
leafletLayer.on("popupopen", () => {
|
||||
State.state.selectedElement.setData(feature)
|
||||
});
|
||||
|
||||
|
||||
this._popups.set(feature, leafletLayer);
|
||||
|
||||
}
|
||||
|
||||
private CreateGeojsonLayer(): L.Layer {
|
||||
|
|
32
Utils.ts
32
Utils.ts
|
@ -189,7 +189,7 @@ export class Utils {
|
|||
* @param z
|
||||
* @param x
|
||||
* @param y
|
||||
* @returns [[lat, lon], [lat, lon]]
|
||||
* @returns [[maxlat, minlon], [minlat, maxlon]]
|
||||
*/
|
||||
static tile_bounds(z: number, x: number, y: number): [[number, number], [number, number]] {
|
||||
return [[Utils.tile2lat(y, z), Utils.tile2long(x, z)], [Utils.tile2lat(y + 1, z), Utils.tile2long(x + 1, z)]]
|
||||
|
@ -201,8 +201,8 @@ export class Utils {
|
|||
static embedded_tile(lat: number, lon: number, z: number): { x: number, y: number, z: number } {
|
||||
return {x: Utils.lon2tile(lon, z), y: Utils.lat2tile(lat, z), z: z}
|
||||
}
|
||||
|
||||
static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1:number, lon1: number) : TileRange{
|
||||
|
||||
static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1: number, lon1: number): TileRange {
|
||||
const t0 = Utils.embedded_tile(lat0, lon0, zoomlevel)
|
||||
const t1 = Utils.embedded_tile(lat1, lon1, zoomlevel)
|
||||
|
||||
|
@ -211,12 +211,12 @@ export class Utils {
|
|||
const ystart = Math.min(t0.y, t1.y)
|
||||
const yend = Math.max(t0.y, t1.y)
|
||||
const total = (1 + xend - xstart) * (1 + yend - ystart)
|
||||
|
||||
|
||||
return {
|
||||
xstart: xstart,
|
||||
xend: xend,
|
||||
ystart: ystart,
|
||||
yend: yend,
|
||||
yend: yend,
|
||||
total: total,
|
||||
zoomlevel: zoomlevel
|
||||
}
|
||||
|
@ -274,8 +274,27 @@ export class Utils {
|
|||
private static lat2tile(lat, zoom) {
|
||||
return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom)));
|
||||
}
|
||||
}
|
||||
|
||||
public static MapRange<T> (tileRange: TileRange, f: (x: number, y: number) => T): T[] {
|
||||
const result : T[] = []
|
||||
for (let x = tileRange.xstart; x <= tileRange.xend; x++) {
|
||||
for (let y = tileRange.ystart; y <= tileRange.yend; y++) {
|
||||
const t= f(x, y);
|
||||
result.push(t)
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static downloadTxtFile (contents: string, fileName: string = "download.txt") {
|
||||
const element = document.createElement("a");
|
||||
const file = new Blob([contents], {type: 'text/plain'});
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = fileName;
|
||||
document.body.appendChild(element); // Required for this to work in FireFox
|
||||
element.click();
|
||||
}
|
||||
}
|
||||
|
||||
export interface TileRange{
|
||||
xstart: number,
|
||||
|
@ -284,4 +303,5 @@ export interface TileRange{
|
|||
yend: number,
|
||||
total: number,
|
||||
zoomlevel: number
|
||||
|
||||
}
|
Loading…
Reference in a new issue