forked from MapComplete/MapComplete
More work on splitting roads, WIP; refactoring tests
This commit is contained in:
parent
e374bb355c
commit
1f93923820
62 changed files with 1163 additions and 823 deletions
|
@ -2,12 +2,13 @@ import {UIEventSource} from "../UIEventSource";
|
|||
import {OsmObject} from "../Osm/OsmObject";
|
||||
import Loc from "../../Models/Loc";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
|
||||
|
||||
/**
|
||||
* Makes sure the hash shows the selected element and vice-versa.
|
||||
*/
|
||||
export default class SelectedFeatureHandler {
|
||||
private static readonly _no_trigger_on = ["welcome", "copyright", "layers", "new"]
|
||||
private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "", undefined])
|
||||
hash: UIEventSource<string>;
|
||||
private readonly state: {
|
||||
selectedElement: UIEventSource<any>
|
||||
|
@ -17,30 +18,35 @@ export default class SelectedFeatureHandler {
|
|||
hash: UIEventSource<string>,
|
||||
state: {
|
||||
selectedElement: UIEventSource<any>,
|
||||
allElements: ElementStorage;
|
||||
allElements: ElementStorage,
|
||||
featurePipeline: FeaturePipeline
|
||||
}
|
||||
) {
|
||||
this.hash = hash;
|
||||
|
||||
this.state = state
|
||||
|
||||
// Getting a blank hash clears the selected element
|
||||
hash.addCallback(h => {
|
||||
|
||||
|
||||
// If the hash changes, set the selected element correctly
|
||||
function setSelectedElementFromHash(h){
|
||||
if (h === undefined || h === "") {
|
||||
// Hash has been cleared - we clear the selected element
|
||||
state.selectedElement.setData(undefined);
|
||||
}else{
|
||||
// we search the element to select
|
||||
const feature = state.allElements.ContainingFeatures.get(h)
|
||||
if(feature !== undefined){
|
||||
state.selectedElement.setData(feature)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
hash.addCallback(setSelectedElementFromHash)
|
||||
|
||||
|
||||
// IF the selected element changes, set the hash correctly
|
||||
state.selectedElement.addCallback(feature => {
|
||||
if (feature === undefined) {
|
||||
console.trace("Resetting hash")
|
||||
if (SelectedFeatureHandler._no_trigger_on.indexOf(hash.data) < 0) {
|
||||
if (SelectedFeatureHandler._no_trigger_on.has(hash.data)) {
|
||||
hash.setData("")
|
||||
}
|
||||
}
|
||||
|
@ -50,13 +56,26 @@ export default class SelectedFeatureHandler {
|
|||
hash.setData(h)
|
||||
}
|
||||
})
|
||||
|
||||
state.featurePipeline.newDataLoadedSignal.addCallbackAndRunD(_ => {
|
||||
// New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet
|
||||
if(hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)){
|
||||
// This is an invalid hash anyway
|
||||
return;
|
||||
}
|
||||
if(state.selectedElement.data !== undefined){
|
||||
// We already have something selected
|
||||
return;
|
||||
}
|
||||
setSelectedElementFromHash(hash.data)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if (hash === undefined || SelectedFeatureHandler._no_trigger_on.has(hash)) {
|
||||
return; // No valid feature selected
|
||||
}
|
||||
// We should have a valid osm-ID and zoom to it... But we wrap it in try-catch to be sure
|
||||
|
|
|
@ -2,65 +2,38 @@ import {UIEventSource} from "../UIEventSource";
|
|||
import Translations from "../../UI/i18n/Translations";
|
||||
import Locale from "../../UI/i18n/Locale";
|
||||
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import Combine from "../../UI/Base/Combine";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
|
||||
class TitleElement extends UIEventSource<string> {
|
||||
export default class TitleHandler {
|
||||
constructor(state) {
|
||||
const currentTitle: UIEventSource<string> = state.selectedElement.map(
|
||||
selected => {
|
||||
console.log("UPdating title")
|
||||
|
||||
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||
private readonly _selectedFeature: UIEventSource<any>;
|
||||
private readonly _allElementsStorage: ElementStorage;
|
||||
|
||||
constructor(layoutToUse: UIEventSource<LayoutConfig>,
|
||||
selectedFeature: UIEventSource<any>,
|
||||
allElementsStorage: ElementStorage) {
|
||||
super("MapComplete");
|
||||
|
||||
this._layoutToUse = layoutToUse;
|
||||
this._selectedFeature = selectedFeature;
|
||||
this._allElementsStorage = allElementsStorage;
|
||||
|
||||
this.syncWith(
|
||||
this._selectedFeature.map(
|
||||
selected => {
|
||||
const defaultTitle = Translations.WT(this._layoutToUse.data?.title)?.txt ?? "MapComplete"
|
||||
|
||||
if (selected === undefined) {
|
||||
return defaultTitle
|
||||
}
|
||||
|
||||
const layout = layoutToUse.data;
|
||||
const tags = selected.properties;
|
||||
|
||||
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.title === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
const tagsSource = allElementsStorage.getEventSourceById(tags.id)
|
||||
const title = new TagRenderingAnswer(tagsSource, layer.title)
|
||||
return new Combine([defaultTitle, " | ", title]).ConstructElement().innerText;
|
||||
}
|
||||
}
|
||||
const layout = state.layoutToUse.data
|
||||
const defaultTitle = Translations.WT(layout?.title)?.txt ?? "MapComplete"
|
||||
|
||||
if (selected === undefined) {
|
||||
return defaultTitle
|
||||
}
|
||||
, [Locale.language, layoutToUse]
|
||||
)
|
||||
|
||||
const tags = selected.properties;
|
||||
for (const layer of layout.layers) {
|
||||
if (layer.title === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
const tagsSource = state.allElements.getEventSourceById(tags.id)
|
||||
const title = new TagRenderingAnswer(tagsSource, layer.title)
|
||||
return new Combine([defaultTitle, " | ", title]).ConstructElement().innerText;
|
||||
}
|
||||
}
|
||||
return defaultTitle
|
||||
}, [Locale.language, state.layoutToUse]
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class TitleHandler {
|
||||
constructor(layoutToUse: UIEventSource<LayoutConfig>,
|
||||
selectedFeature: UIEventSource<any>,
|
||||
allElementsStorage: ElementStorage) {
|
||||
new TitleElement(layoutToUse, selectedFeature, allElementsStorage).addCallbackAndRunD(title => {
|
||||
currentTitle.addCallbackAndRunD(title => {
|
||||
document.title = title
|
||||
})
|
||||
}
|
||||
|
|
|
@ -88,7 +88,7 @@ export class ExtraFunction {
|
|||
{
|
||||
name: "distanceTo",
|
||||
doc: "Calculates the distance between the feature and a specified point in kilometer. The input should either be a pair of coordinates, a geojson feature or the ID of an object",
|
||||
args: ["longitude", "latitude"]
|
||||
args: ["feature OR featureID OR longitude", "undefined OR latitude"]
|
||||
},
|
||||
(featuresPerLayer, feature) => {
|
||||
return (arg0, lat) => {
|
||||
|
@ -128,10 +128,10 @@ export class ExtraFunction {
|
|||
private static readonly ClosestNObjectFunc = new ExtraFunction(
|
||||
{
|
||||
name: "closestn",
|
||||
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" +
|
||||
doc: "Given either a list of geojson features or a single layer name, gives the n closest objects which are nearest to the feature (excluding the feature itself). 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 loaded)\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)", "maxDistanceInMeters (optional)"]
|
||||
args: ["list of features or layer name", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"]
|
||||
},
|
||||
(params, feature) => {
|
||||
return (features, amount, uniqueTag, maxDistanceInMeters) => ExtraFunction.GetClosestNFeatures(params, feature, features, {
|
||||
|
|
|
@ -4,15 +4,13 @@
|
|||
* Technically, more an Actor then a featuresource, but it fits more neatly this ay
|
||||
*/
|
||||
import {FeatureSourceForLayer} from "../FeatureSource";
|
||||
import {Utils} from "../../../Utils";
|
||||
|
||||
export default class LocalStorageSaverActor {
|
||||
export default class SaveTileToLocalStorageActor {
|
||||
public static readonly storageKey: string = "cached-features";
|
||||
|
||||
constructor(source: FeatureSourceForLayer, x: number, y: number, z: number) {
|
||||
constructor(source: FeatureSourceForLayer, tileIndex: number) {
|
||||
source.features.addCallbackAndRunD(features => {
|
||||
const index = Utils.tile_index(z, x, y)
|
||||
const key = `${LocalStorageSaverActor.storageKey}-${source.layer.layerDef.id}-${index}`
|
||||
const key = `${SaveTileToLocalStorageActor.storageKey}-${source.layer.layerDef.id}-${tileIndex}`
|
||||
const now = new Date().getTime()
|
||||
|
||||
if (features.length == 0) {
|
|
@ -1,202 +1,85 @@
|
|||
import FeatureSource, {IndexedFeatureSource} from "./FeatureSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource} from "./FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Changes} from "../Osm/Changes";
|
||||
import {ChangeDescription} from "../Osm/Actions/ChangeDescription";
|
||||
import {ChangeDescription, ChangeDescriptionTools} from "../Osm/Actions/ChangeDescription";
|
||||
import {Utils} from "../../Utils";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject";
|
||||
|
||||
/**
|
||||
* A feature source containing exclusively new elements
|
||||
*/
|
||||
export class NewGeometryChangeApplicatorFeatureSource implements FeatureSource{
|
||||
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
public readonly name: string = "newFeatures";
|
||||
constructor(changes: Changes) {
|
||||
const seenChanges = new Set<ChangeDescription>();
|
||||
changes.pendingChanges.addCallbackAndRunD(changes => {
|
||||
for (const change of changes) {
|
||||
if(seenChanges.has(change)){
|
||||
continue
|
||||
}
|
||||
seenChanges.add(change)
|
||||
|
||||
if(change.id < 0){
|
||||
// This is a new object!
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies changes from 'Changes' onto a featureSource
|
||||
* Applies geometry changes from 'Changes' onto every feature of a featureSource
|
||||
*/
|
||||
export default class ChangeApplicator implements FeatureSource {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string;
|
||||
private readonly source: IndexedFeatureSource;
|
||||
private readonly changes: Changes;
|
||||
private readonly mode?: {
|
||||
generateNewGeometries: boolean
|
||||
};
|
||||
public readonly layer: FilteredLayer
|
||||
|
||||
constructor(source: IndexedFeatureSource, changes: Changes, mode?: {
|
||||
generateNewGeometries: boolean
|
||||
}) {
|
||||
constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) {
|
||||
this.source = source;
|
||||
this.changes = changes;
|
||||
this.mode = mode;
|
||||
this.layer = source.layer
|
||||
|
||||
this.name = "ChangesApplied(" + source.name + ")"
|
||||
this.features = source.features
|
||||
const seenChanges = new Set<ChangeDescription>();
|
||||
this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined)
|
||||
|
||||
const self = this;
|
||||
let runningUpdate = false;
|
||||
source.features.addCallbackAndRunD(features => {
|
||||
if (runningUpdate) {
|
||||
return; // No need to ping again
|
||||
}
|
||||
self.ApplyChanges()
|
||||
seenChanges.clear()
|
||||
})
|
||||
source.features.addCallbackAndRunD(_ => self.update())
|
||||
|
||||
changes.allChanges.addCallbackAndRunD(_ => self.update())
|
||||
|
||||
changes.pendingChanges.addCallbackAndRunD(changes => {
|
||||
runningUpdate = true;
|
||||
changes = changes.filter(ch => !seenChanges.has(ch))
|
||||
changes.forEach(c => seenChanges.add(c))
|
||||
self.ApplyChanges()
|
||||
source.features.ping()
|
||||
runningUpdate = false;
|
||||
})
|
||||
}
|
||||
|
||||
private update() {
|
||||
const upstreamFeatures = this.source.features.data
|
||||
const upstreamIds = this.source.containedIds.data
|
||||
const changesToApply = this.changes.allChanges.data
|
||||
?.filter(ch =>
|
||||
// Does upsteram have this element? If not, we skip
|
||||
upstreamIds.has(ch.type + "/" + ch.id) &&
|
||||
// Are any (geometry) changes defined?
|
||||
ch.changes !== undefined &&
|
||||
// Ignore new elements, they are handled by the NewGeometryFromChangesFeatureSource
|
||||
ch.id > 0)
|
||||
|
||||
/**
|
||||
* Returns true if the geometry is changed and the source should be pinged
|
||||
*/
|
||||
private ApplyChanges(): boolean {
|
||||
const cs = this.changes.pendingChanges.data
|
||||
const features = this.source.features.data
|
||||
const loadedIds = this.source.containedIds
|
||||
if (cs.length === 0 || features === undefined) {
|
||||
if (changesToApply === undefined || changesToApply.length === 0) {
|
||||
// No changes to apply!
|
||||
// Pass the original feature and lets continue our day
|
||||
this.features.setData(upstreamFeatures);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Applying changes ", this.name, cs)
|
||||
let geometryChanged = false;
|
||||
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
|
||||
const changesPerId = new Map<string, ChangeDescription[]>()
|
||||
for (const ch of changesToApply) {
|
||||
const key = ch.type + "/" + ch.id
|
||||
if(changesPerId.has(key)){
|
||||
changesPerId.get(key).push(ch)
|
||||
}else{
|
||||
changesPerId.set(key, [ch])
|
||||
}
|
||||
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()
|
||||
|
||||
function add(feature) {
|
||||
feature.id = feature.properties.id
|
||||
features.push({
|
||||
feature: feature,
|
||||
freshness: now
|
||||
})
|
||||
console.log("Added a new feature: ", feature)
|
||||
geometryChanged = true;
|
||||
}
|
||||
|
||||
// First, create the new features - they have a negative ID
|
||||
// We don't set the properties yet though
|
||||
if (this.mode?.generateNewGeometries) {
|
||||
changesPerId.forEach(cs => {
|
||||
cs
|
||||
.forEach(change => {
|
||||
if (change.id >= 0) {
|
||||
return; // Nothing to do here, already created
|
||||
}
|
||||
|
||||
if (change.changes === undefined) {
|
||||
// An update to the object - not the actual created
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
n.lat = change.changes["lat"]
|
||||
n.lon = change.changes["lon"]
|
||||
const geojson = n.asGeoJson()
|
||||
add(geojson)
|
||||
break;
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.nodes = change.changes["nodes"]
|
||||
add(w.asGeoJson())
|
||||
break;
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.members = change.changes["members"]
|
||||
add(r.asGeoJson())
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
for (const feature of features) {
|
||||
const f = feature.feature;
|
||||
const id = f.properties.id;
|
||||
if (!changesPerId.has(id)) {
|
||||
const newFeatures: { feature: any, freshness: Date }[] = []
|
||||
for (const feature of upstreamFeatures) {
|
||||
const changesForFeature = changesPerId.get(feature.feature.properties.id)
|
||||
if (changesForFeature === undefined) {
|
||||
// No changes for this element
|
||||
newFeatures.push(feature)
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const changed = {}
|
||||
// Copy all the properties
|
||||
Utils.Merge(f, changed)
|
||||
// play the changes onto the copied object
|
||||
|
||||
for (const change of changesPerId.get(id)) {
|
||||
for (const kv of change.tags ?? []) {
|
||||
// Apply tag changes and ping the consumers
|
||||
f.properties[kv.k] = kv.v;
|
||||
}
|
||||
|
||||
// Apply other changes to the object
|
||||
if (change.changes !== undefined) {
|
||||
geometryChanged = true;
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
// @ts-ignore
|
||||
const coor: { lat, lon } = change.changes;
|
||||
f.geometry.coordinates = [coor.lon, coor.lat]
|
||||
break;
|
||||
case "way":
|
||||
f.geometry.coordinates = change.changes["locations"]
|
||||
break;
|
||||
case "relation":
|
||||
console.error("Changes to relations are not yet supported")
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Allright! We have a feature to rewrite!
|
||||
const copy = {
|
||||
...feature
|
||||
}
|
||||
// We only apply the last change as that one'll have the latest geometry
|
||||
const change = changesForFeature[changesForFeature.length - 1]
|
||||
copy.feature.geometry = ChangeDescriptionTools.getGeojsonGeometry(change)
|
||||
newFeatures.push(copy)
|
||||
}
|
||||
return geometryChanged
|
||||
this.features.setData(newFeatures)
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import FilteringFeatureSource from "./Sources/FilteringFeatureSource";
|
||||
import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter";
|
||||
import FeatureSource, {FeatureSourceForLayer, FeatureSourceState, Tiled} from "./FeatureSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, FeatureSourceState, IndexedFeatureSource, Tiled} from "./FeatureSource";
|
||||
import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy";
|
||||
|
@ -14,13 +14,14 @@ 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 SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor";
|
||||
import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource";
|
||||
import {BBox} from "../GeoOperations";
|
||||
import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger";
|
||||
import RelationsTracker from "../Osm/RelationsTracker";
|
||||
import {NewGeometryFromChangesFeatureSource} from "./Sources/NewGeometryFromChangesFeatureSource";
|
||||
import ChangeGeometryApplicator from "./ChangeApplicator";
|
||||
|
||||
|
||||
export default class FeaturePipeline implements FeatureSourceState {
|
||||
|
@ -29,10 +30,12 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
public readonly runningQuery: UIEventSource<boolean>;
|
||||
public readonly timeout: UIEventSource<number>;
|
||||
public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly newDataLoadedSignal: UIEventSource<FeatureSource> = new UIEventSource<FeatureSource>(undefined)
|
||||
|
||||
private readonly overpassUpdater: OverpassFeatureSource
|
||||
private readonly relationTracker: RelationsTracker
|
||||
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>;
|
||||
|
||||
constructor(
|
||||
handleFeatureSource: (source: FeatureSourceForLayer) => void,
|
||||
state: {
|
||||
|
@ -49,6 +52,7 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
|
||||
const self = this
|
||||
const updater = new OverpassFeatureSource(state);
|
||||
updater.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(updater))
|
||||
this.overpassUpdater = updater;
|
||||
this.sufficientlyZoomed = updater.sufficientlyZoomed
|
||||
this.runningQuery = updater.runningQuery
|
||||
|
@ -56,16 +60,17 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
this.relationTracker = updater.relationsTracker
|
||||
// Register everything in the state' 'AllElements'
|
||||
new RegisteringAllFromFeatureSourceActor(updater)
|
||||
|
||||
|
||||
const perLayerHierarchy = new Map<string, TileHierarchyMerger>()
|
||||
this.perLayerHierarchy = perLayerHierarchy
|
||||
|
||||
const patchedHandleFeatureSource = function (src: FeatureSourceForLayer) {
|
||||
const patchedHandleFeatureSource = function (src: FeatureSourceForLayer & IndexedFeatureSource) {
|
||||
// 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,
|
||||
new ChangeGeometryApplicator(src, state.changes)
|
||||
)
|
||||
)
|
||||
handleFeatureSource(srcFiltered)
|
||||
|
@ -90,6 +95,7 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
(src) => {
|
||||
new RegisteringAllFromFeatureSourceActor(src)
|
||||
hierarchy.registerTile(src);
|
||||
src.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(src))
|
||||
}, state)
|
||||
continue
|
||||
}
|
||||
|
@ -103,6 +109,7 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
registerTile: (tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile)
|
||||
addToHierarchy(tile, id)
|
||||
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
@ -113,6 +120,7 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
registerTile: (tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile)
|
||||
addToHierarchy(tile, id)
|
||||
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
|
||||
}
|
||||
}),
|
||||
state
|
||||
|
@ -122,64 +130,90 @@ export default class FeaturePipeline implements FeatureSourceState {
|
|||
}
|
||||
|
||||
// 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)
|
||||
new SaveTileToLocalStorageActor(tile, tile.tileIndex)
|
||||
addToHierarchy(tile, source.layer.layerDef.id);
|
||||
}
|
||||
}), new RememberingSource(updater))
|
||||
}),
|
||||
new RememberingSource(updater))
|
||||
|
||||
|
||||
// Also load points/lines that are newly added.
|
||||
const newGeometry = new NewGeometryFromChangesFeatureSource(state.changes)
|
||||
new RegisteringAllFromFeatureSourceActor(newGeometry)
|
||||
// A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
|
||||
new PerLayerFeatureSourceSplitter(state.filteredLayers,
|
||||
(perLayer) => {
|
||||
// We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
|
||||
addToHierarchy(perLayer, perLayer.layer.layerDef.id)
|
||||
// AT last, we always apply the metatags whenever possible
|
||||
perLayer.features.addCallbackAndRunD(_ => self.applyMetaTags(perLayer))
|
||||
},
|
||||
newGeometry
|
||||
)
|
||||
|
||||
|
||||
// Whenever fresh data comes in, we need to update the metatagging
|
||||
updater.features.addCallback(_ => {
|
||||
self.newDataLoadedSignal.stabilized(1000).addCallback(src => {
|
||||
console.log("Got an update from ", src.name)
|
||||
self.updateAllMetaTagging()
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private applyMetaTags(src: FeatureSourceForLayer){
|
||||
const self = this
|
||||
MetaTagging.addMetatags(
|
||||
src.features.data,
|
||||
{
|
||||
memberships: this.relationTracker,
|
||||
getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox)
|
||||
},
|
||||
src.layer.layerDef,
|
||||
{
|
||||
includeDates: true,
|
||||
// We assume that the non-dated metatags are already set by the cache generator
|
||||
includeNonDates: !src.layer.layerDef.source.isOsmCacheLayer
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
self.applyMetaTags(src)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public GetAllFeaturesWithin(bbox: BBox): any[][]{
|
||||
|
||||
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[][]{
|
||||
|
||||
public GetFeaturesWithin(layerId: string, bbox: BBox): any[][] {
|
||||
const requestedHierarchy = this.perLayerHierarchy.get(layerId)
|
||||
if (requestedHierarchy === undefined) {
|
||||
console.warn("Layer ", layerId, "is not defined. Try one of ", Array.from(this.perLayerHierarchy.keys()))
|
||||
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 => {
|
||||
|
||||
public GetTilesPerLayerWithin(bbox: BBox, handleTile: (tile: FeatureSourceForLayer & Tiled) => void) {
|
||||
Array.from(this.perLayerHierarchy.values()).forEach(hierarchy => {
|
||||
TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import FeatureSource, {FeatureSourceForLayer} from "./FeatureSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
|
||||
|
@ -12,10 +12,13 @@ import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
|
|||
export default class PerLayerFeatureSourceSplitter {
|
||||
|
||||
constructor(layers: UIEventSource<FilteredLayer[]>,
|
||||
handleLayerData: (source: FeatureSourceForLayer) => void,
|
||||
upstream: FeatureSource) {
|
||||
handleLayerData: (source: FeatureSourceForLayer & Tiled) => void,
|
||||
upstream: FeatureSource,
|
||||
options?:{
|
||||
handleLeftovers?: (featuresWithoutLayer: any[]) => void
|
||||
}) {
|
||||
|
||||
const knownLayers = new Map<string, FeatureSourceForLayer>()
|
||||
const knownLayers = new Map<string, FeatureSourceForLayer & Tiled>()
|
||||
|
||||
function update() {
|
||||
const features = upstream.features.data;
|
||||
|
@ -30,7 +33,7 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
// Note that this splitter is only run when it is invoked by the overpass feature source, so we can't be sure in which layer it should go
|
||||
|
||||
const featuresPerLayer = new Map<string, { feature, freshness } []>();
|
||||
|
||||
const noLayerFound = []
|
||||
function addTo(layer: FilteredLayer, feature: { feature, freshness }) {
|
||||
const id = layer.layerDef.id
|
||||
const list = featuresPerLayer.get(id)
|
||||
|
@ -51,6 +54,7 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
break;
|
||||
}
|
||||
}
|
||||
noLayerFound.push(f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,6 +79,11 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
featureSource.features.setData(features)
|
||||
}
|
||||
}
|
||||
|
||||
// AT last, the leftovers are handled
|
||||
if(options?.handleLeftovers !== undefined && noLayerFound.length > 0){
|
||||
options.handleLeftovers(noLayerFound)
|
||||
}
|
||||
}
|
||||
|
||||
layers.addCallback(_ => update())
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
* Uses the freshest feature available in the case multiple sources offer data with the same identifier
|
||||
*/
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {BBox} from "../../GeoOperations";
|
||||
import {Utils} from "../../../Utils";
|
||||
|
||||
export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled {
|
||||
export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource {
|
||||
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name;
|
||||
|
@ -16,6 +16,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
|
|||
private readonly _sources: UIEventSource<FeatureSource[]>;
|
||||
public readonly tileIndex: number;
|
||||
public readonly bbox: BBox;
|
||||
public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>(new Set())
|
||||
|
||||
constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
|
||||
this.tileIndex = tileIndex;
|
||||
|
@ -54,7 +55,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
|
|||
// 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.feature._matching_layer_id, oldValue)
|
||||
all.set(oldValue.feature.id, oldValue)
|
||||
}
|
||||
|
||||
for (const source of this._sources.data) {
|
||||
|
@ -62,7 +63,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
|
|||
continue;
|
||||
}
|
||||
for (const f of source.features.data) {
|
||||
const id = f.feature.properties.id + f.feature._matching_layer_id;
|
||||
const id = f.feature.properties.id;
|
||||
if (!all.has(id)) {
|
||||
// This is a new feature
|
||||
somethingChanged = true;
|
||||
|
@ -90,6 +91,7 @@ export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled
|
|||
all.forEach((value, _) => {
|
||||
newList.push(value)
|
||||
})
|
||||
this.containedIds.setData(new Set(all.keys()))
|
||||
this.features.setData(newList);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import {Changes} from "../../Osm/Changes";
|
||||
import {OsmNode, OsmRelation, OsmWay} from "../../Osm/OsmObject";
|
||||
import FeatureSource from "../FeatureSource";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import {ChangeDescription} from "../../Osm/Actions/ChangeDescription";
|
||||
|
||||
export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
||||
// This class name truly puts the 'Java' into 'Javascript'
|
||||
|
||||
/**
|
||||
* A feature source containing exclusively new elements
|
||||
*/
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string = "newFeatures";
|
||||
|
||||
constructor(changes: Changes) {
|
||||
|
||||
const seenChanges = new Set<ChangeDescription>();
|
||||
const features = this.features.data;
|
||||
const self = this;
|
||||
|
||||
changes.pendingChanges
|
||||
.map(changes => changes.filter(ch =>
|
||||
// only new objects allowed
|
||||
ch.id < 0 &&
|
||||
// The change is an update to the object (e.g. tags or geometry) - not the actual create
|
||||
ch.changes !== undefined &&
|
||||
// If tags is undefined, this is probably a new point that is part of a split road
|
||||
ch.tags !== undefined &&
|
||||
// Already handled
|
||||
!seenChanges.has(ch)))
|
||||
.addCallbackAndRunD(changes => {
|
||||
|
||||
if (changes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
|
||||
function add(feature) {
|
||||
feature.id = feature.properties.id
|
||||
features.push({
|
||||
feature: feature,
|
||||
freshness: now
|
||||
})
|
||||
console.warn("Added a new feature: ", JSON.stringify(feature))
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
seenChanges.add(change)
|
||||
try {
|
||||
const tags = {}
|
||||
for (const kv of change.tags) {
|
||||
tags[kv.k] = kv.v
|
||||
}
|
||||
tags["id"] = change.type+"/"+change.id
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
n.tags = tags
|
||||
n.lat = change.changes["lat"]
|
||||
n.lon = change.changes["lon"]
|
||||
const geojson = n.asGeoJson()
|
||||
add(geojson)
|
||||
break;
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.tags = tags
|
||||
w.nodes = change.changes["nodes"]
|
||||
w.coordinates = change.changes["coordinates"].map(coor => coor.reverse())
|
||||
add(w.asGeoJson())
|
||||
break;
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.tags = tags
|
||||
r.members = change.changes["members"]
|
||||
add(r.asGeoJson())
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not generate a new geometry to render on screen for:", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
self.features.ping()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +1,19 @@
|
|||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer} from "../FeatureSource";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {BBox} from "../../GeoOperations";
|
||||
import {Utils} from "../../../Utils";
|
||||
|
||||
export default class SimpleFeatureSource implements FeatureSourceForLayer {
|
||||
export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string = "SimpleFeatureSource";
|
||||
public readonly layer: FilteredLayer;
|
||||
public readonly bbox: BBox = BBox.global;
|
||||
public readonly tileIndex: number = Utils.tile_index(0, 0, 0);
|
||||
|
||||
constructor(layer: FilteredLayer) {
|
||||
this.name = "SimpleFeatureSource("+layer.layerDef.id+")"
|
||||
this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")"
|
||||
this.layer = layer
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import TileHierarchy from "./TileHierarchy";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {BBox} from "../../GeoOperations";
|
||||
|
@ -11,9 +11,9 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer
|
|||
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>();
|
||||
|
||||
public readonly layer: FilteredLayer;
|
||||
private _handleTile: (src: FeatureSourceForLayer, index: number) => void;
|
||||
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void;
|
||||
|
||||
constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer, index: number) => void) {
|
||||
constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void) {
|
||||
this.layer = layer;
|
||||
this._handleTile = handleTile;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import {UIEventSource} from "../../UIEventSource";
|
|||
import Loc from "../../../Models/Loc";
|
||||
import TileHierarchy from "./TileHierarchy";
|
||||
import {Utils} from "../../../Utils";
|
||||
import LocalStorageSaverActor from "../Actors/LocalStorageSaverActor";
|
||||
import SaveTileToLocalStorageActor from "../Actors/SaveTileToLocalStorageActor";
|
||||
import {BBox} from "../../GeoOperations";
|
||||
|
||||
export default class TiledFromLocalStorageSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
|
||||
|
@ -17,7 +17,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
|
|||
leafletMap: any
|
||||
}) {
|
||||
|
||||
const prefix = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-"
|
||||
const prefix = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-"
|
||||
// @ts-ignore
|
||||
const indexes: number[] = Object.keys(localStorage)
|
||||
.filter(key => {
|
||||
|
@ -76,7 +76,7 @@ export default class TiledFromLocalStorageSource implements TileHierarchy<Featur
|
|||
for (const neededIndex of neededIndexes) {
|
||||
// We load the features from localStorage
|
||||
try {
|
||||
const key = LocalStorageSaverActor.storageKey + "-" + layer.layerDef.id + "-" + neededIndex
|
||||
const key = SaveTileToLocalStorageActor.storageKey + "-" + layer.layerDef.id + "-" + neededIndex
|
||||
const data = localStorage.getItem(key)
|
||||
const features = JSON.parse(data)
|
||||
const src = {
|
||||
|
|
|
@ -185,14 +185,14 @@ export class GeoOperations {
|
|||
static lengthInMeters(feature: any) {
|
||||
return turf.length(feature) * 1000
|
||||
}
|
||||
|
||||
static buffer(feature: any, bufferSizeInMeter: number){
|
||||
return turf.buffer(feature, bufferSizeInMeter/1000, {
|
||||
|
||||
static buffer(feature: any, bufferSizeInMeter: number) {
|
||||
return turf.buffer(feature, bufferSizeInMeter / 1000, {
|
||||
units: 'kilometers'
|
||||
})
|
||||
}
|
||||
|
||||
static bbox(feature: any){
|
||||
|
||||
static bbox(feature: any) {
|
||||
const [lon, lat, lon0, lat0] = turf.bbox(feature)
|
||||
return {
|
||||
"type": "Feature",
|
||||
|
@ -226,6 +226,11 @@ export class GeoOperations {
|
|||
|
||||
/**
|
||||
* Generates the closest point on a way from a given point
|
||||
*
|
||||
* The properties object will contain three values:
|
||||
// - `index`: closest point was found on nth line part,
|
||||
// - `dist`: distance between pt and the closest point (in kilometer),
|
||||
// `location`: distance along the line between start and the closest point.
|
||||
* @param way The road on which you want to find a point
|
||||
* @param point Point defined as [lon, lat]
|
||||
*/
|
||||
|
@ -379,7 +384,7 @@ export class BBox {
|
|||
readonly maxLon: number;
|
||||
readonly minLat: number;
|
||||
readonly minLon: number;
|
||||
static global: BBox = new BBox([[-180,-90],[180,90]]);
|
||||
static global: BBox = new BBox([[-180, -90], [180, 90]]);
|
||||
|
||||
constructor(coordinates) {
|
||||
this.maxLat = Number.MIN_VALUE;
|
||||
|
@ -447,7 +452,7 @@ export class BBox {
|
|||
}
|
||||
|
||||
static fromTile(z: number, x: number, y: number) {
|
||||
return new BBox( Utils.tile_bounds_lon_lat(z, x, y))
|
||||
return new BBox(Utils.tile_bounds_lon_lat(z, x, y))
|
||||
}
|
||||
|
||||
getEast() {
|
||||
|
@ -465,4 +470,20 @@ export class BBox {
|
|||
getSouth() {
|
||||
return this.minLat
|
||||
}
|
||||
|
||||
pad(factor: number) : BBox {
|
||||
const latDiff = this.maxLat - this.minLat
|
||||
const lat = (this.maxLat + this.minLat) / 2
|
||||
const lonDiff = this.maxLon - this.minLon
|
||||
const lon = (this.maxLon + this.minLon) / 2
|
||||
return new BBox([[
|
||||
lon - lonDiff * factor,
|
||||
lat - latDiff * factor
|
||||
], [lon + lonDiff * factor,
|
||||
lat + latDiff * factor]])
|
||||
}
|
||||
|
||||
toLeaflet() {
|
||||
return [[this.minLat, this.minLon], [this.maxLat, this.maxLon]]
|
||||
}
|
||||
}
|
|
@ -19,22 +19,31 @@ export default class MetaTagging {
|
|||
* 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 }[],
|
||||
public static addMetatags(features: { feature: any; freshness: Date }[],
|
||||
params: ExtraFuncParams,
|
||||
layer: LayerConfig,
|
||||
includeDates = true) {
|
||||
options?: {
|
||||
includeDates?: true | boolean,
|
||||
includeNonDates?: true | boolean
|
||||
}) {
|
||||
|
||||
if (features === undefined || features.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const metatag of SimpleMetaTagger.metatags) {
|
||||
if (metatag.includesDates && !includeDates) {
|
||||
// We do not add dated entries
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
metatag.addMetaTags(features);
|
||||
if (metatag.includesDates) {
|
||||
if (options.includeDates ?? true) {
|
||||
metatag.addMetaTags(features);
|
||||
}
|
||||
} else {
|
||||
if (options.includeNonDates ?? true) {
|
||||
metatag.addMetaTags(features);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Could not calculate metatag for ", metatag.keys.join(","), ":", e)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {OsmNode, OsmRelation, OsmWay} from "../OsmObject";
|
||||
|
||||
/**
|
||||
* Represents a single change to an object
|
||||
*/
|
||||
|
@ -29,8 +31,8 @@ export interface ChangeDescription {
|
|||
lat: number,
|
||||
lon: number
|
||||
} | {
|
||||
// Coordinates are only used for rendering. They should be lon, lat
|
||||
locations: [number, number][]
|
||||
// Coordinates are only used for rendering. They should be LAT, LON
|
||||
coordinates: [number, number][]
|
||||
nodes: number[],
|
||||
} | {
|
||||
members: { type: "node" | "way" | "relation", ref: number, role: string }[]
|
||||
|
@ -40,6 +42,26 @@ export interface ChangeDescription {
|
|||
Set to delete the object
|
||||
*/
|
||||
doDelete?: boolean
|
||||
}
|
||||
|
||||
|
||||
export class ChangeDescriptionTools{
|
||||
|
||||
public static getGeojsonGeometry(change: ChangeDescription): any{
|
||||
switch (change.type) {
|
||||
case "node":
|
||||
const n = new OsmNode(change.id)
|
||||
n.lat = change.changes["lat"]
|
||||
n.lon = change.changes["lon"]
|
||||
return n.asGeoJson().geometry
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.nodes = change.changes["nodes"]
|
||||
w.coordinates = change.changes["coordinates"].map(coor => coor.reverse())
|
||||
return w.asGeoJson().geometry
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.members = change.changes["members"]
|
||||
return r.asGeoJson().geometry
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ export default class ChangeTagAction extends OsmChangeAction {
|
|||
return {k: key.trim(), v: value.trim()};
|
||||
}
|
||||
|
||||
CreateChangeDescriptions(changes: Changes): ChangeDescription [] {
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const changedTags: { k: string, v: string }[] = this._tagsFilter.asChange(this._currentTags).map(ChangeTagAction.checkChange)
|
||||
const typeId = this._elementId.split("/")
|
||||
const type = typeId[0]
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
|||
this._reusePointDistance = options?.reusePointWithinMeters ?? 1
|
||||
}
|
||||
|
||||
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const id = changes.getNewID()
|
||||
const properties = {
|
||||
id: "node/" + id
|
||||
|
@ -97,7 +97,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
|||
type: "way",
|
||||
id: this._snapOnto.id,
|
||||
changes: {
|
||||
locations: locations,
|
||||
coordinates: locations,
|
||||
nodes: ids
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ export default class DeleteAction {
|
|||
canBeDeleted: false,
|
||||
reason: t.notEnoughExperience
|
||||
})
|
||||
return;
|
||||
return true; // unregister this caller!
|
||||
}
|
||||
|
||||
if (!useTheInternet) {
|
||||
|
@ -167,13 +167,14 @@ export default class DeleteAction {
|
|||
}
|
||||
|
||||
// All right! We have arrived at a point that we should query OSM again to check that the point isn't a part of ways or relations
|
||||
OsmObject.DownloadReferencingRelations(id).addCallbackAndRunD(rels => {
|
||||
OsmObject.DownloadReferencingRelations(id).then(rels => {
|
||||
hasRelations.setData(rels.length > 0)
|
||||
})
|
||||
|
||||
OsmObject.DownloadReferencingWays(id).addCallbackAndRunD(ways => {
|
||||
OsmObject.DownloadReferencingWays(id).then(ways => {
|
||||
hasWays.setData(ways.length > 0)
|
||||
})
|
||||
return true; // unregister to only run once
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ export default abstract class OsmChangeAction {
|
|||
return this.CreateChangeDescriptions(changes)
|
||||
}
|
||||
|
||||
protected abstract CreateChangeDescriptions(changes: Changes): ChangeDescription[]
|
||||
protected abstract CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]>
|
||||
|
||||
|
||||
}
|
142
Logic/Osm/Actions/RelationSplitHandler.ts
Normal file
142
Logic/Osm/Actions/RelationSplitHandler.ts
Normal file
|
@ -0,0 +1,142 @@
|
|||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {OsmObject, OsmRelation, OsmWay} from "../OsmObject";
|
||||
|
||||
export interface RelationSplitInput {
|
||||
relation: OsmRelation,
|
||||
originalWayId: number,
|
||||
allWayIdsInOrder: number[],
|
||||
originalNodes: number[],
|
||||
allWaysNodesInOrder: number[][]
|
||||
}
|
||||
|
||||
/**
|
||||
* When a way is split and this way is part of a relation, the relation should be updated too to have the new segment if relevant.
|
||||
*/
|
||||
export default class RelationSplitHandler extends OsmChangeAction {
|
||||
|
||||
constructor(input: RelationSplitInput) {
|
||||
super()
|
||||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A simple strategy to split relations:
|
||||
* -> Download the way members just before and just after the original way
|
||||
* -> Make sure they are still aligned
|
||||
*
|
||||
* Note that the feature might appear multiple times.
|
||||
*/
|
||||
export class InPlaceReplacedmentRTSH extends OsmChangeAction {
|
||||
private readonly _input: RelationSplitInput;
|
||||
|
||||
constructor(input: RelationSplitInput) {
|
||||
super();
|
||||
this._input = input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns which node should border the member at the given index
|
||||
*/
|
||||
private async targetNodeAt(i: number, first: boolean) {
|
||||
const member = this._input.relation.members[i]
|
||||
if (member === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (member.type === "node") {
|
||||
return member.ref
|
||||
}
|
||||
if (member.type === "way") {
|
||||
const osmWay = <OsmWay>await OsmObject.DownloadObjectAsync("way/" + member.ref)
|
||||
const nodes = osmWay.nodes
|
||||
if (first) {
|
||||
return nodes[0]
|
||||
} else {
|
||||
return nodes[nodes.length - 1]
|
||||
}
|
||||
}
|
||||
if (member.type === "relation") {
|
||||
return undefined
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
|
||||
const wayId = this._input.originalWayId
|
||||
const relation = this._input.relation
|
||||
const members = relation.members
|
||||
const originalNodes = this._input.originalNodes;
|
||||
const firstNode = originalNodes[0]
|
||||
const lastNode = originalNodes[originalNodes.length - 1]
|
||||
const newMembers: { type: "node" | "way" | "relation", ref: number, role: string }[] = []
|
||||
|
||||
for (let i = 0; i < members.length; i++) {
|
||||
const member = members[i];
|
||||
if (member.type !== "way" || member.ref !== wayId) {
|
||||
newMembers.push(member)
|
||||
continue;
|
||||
}
|
||||
|
||||
const nodeIdBefore = await this.targetNodeAt(i - 1, false)
|
||||
const nodeIdAfter = await this.targetNodeAt(i + 1, true)
|
||||
|
||||
const firstNodeMatches = nodeIdBefore === undefined || nodeIdBefore === firstNode
|
||||
const lastNodeMatches =nodeIdAfter === undefined || nodeIdAfter === lastNode
|
||||
|
||||
if (firstNodeMatches && lastNodeMatches) {
|
||||
// We have a classic situation, forward situation
|
||||
for (const wId of this._input.allWayIdsInOrder) {
|
||||
newMembers.push({
|
||||
ref: wId,
|
||||
type: "way",
|
||||
role: member.role
|
||||
})
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstNodeMatchesRev = nodeIdBefore === undefined || nodeIdBefore === lastNode
|
||||
const lastNodeMatchesRev =nodeIdAfter === undefined || nodeIdAfter === firstNode
|
||||
if (firstNodeMatchesRev || lastNodeMatchesRev) {
|
||||
// We (probably) have a reversed situation, backward situation
|
||||
for (let i1 = this._input.allWayIdsInOrder.length - 1; i1 >= 0; i1--){
|
||||
// Iterate BACKWARDS
|
||||
const wId = this._input.allWayIdsInOrder[i1];
|
||||
newMembers.push({
|
||||
ref: wId,
|
||||
type: "way",
|
||||
role: member.role
|
||||
})
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Euhm, allright... Something weird is going on, but let's not care too much
|
||||
// Lets pretend this is forward going
|
||||
for (const wId of this._input.allWayIdsInOrder) {
|
||||
newMembers.push({
|
||||
ref: wId,
|
||||
type: "way",
|
||||
role: member.role
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [{
|
||||
id: relation.id,
|
||||
type: "relation",
|
||||
changes: {members: newMembers}
|
||||
}];
|
||||
}
|
||||
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/**
|
||||
* The logic to handle relations after a way within
|
||||
*/
|
||||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {OsmRelation} from "../OsmObject";
|
||||
|
||||
export default class RelationSplitlHandler extends OsmChangeAction {
|
||||
|
||||
constructor(partOf: OsmRelation[], newWayIds: number[], originalNodes: number[]) {
|
||||
super()
|
||||
}
|
||||
|
||||
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import {OsmRelation, OsmWay} from "../OsmObject";
|
||||
import {OsmObject, OsmWay} from "../OsmObject";
|
||||
import {Changes} from "../Changes";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import RelationSplitlHandler from "./RelationSplitlHandler";
|
||||
import RelationSplitHandler from "./RelationSplitHandler";
|
||||
|
||||
interface SplitInfo {
|
||||
originalIndex?: number, // or negative for new elements
|
||||
|
@ -12,17 +12,13 @@ interface SplitInfo {
|
|||
}
|
||||
|
||||
export default class SplitAction extends OsmChangeAction {
|
||||
private readonly roadObject: any;
|
||||
private readonly osmWay: OsmWay;
|
||||
private _partOf: OsmRelation[];
|
||||
private readonly _splitPoints: any[];
|
||||
private readonly wayId: string;
|
||||
private readonly _splitPointsCoordinates: [number, number] []// lon, lat
|
||||
|
||||
constructor(osmWay: OsmWay, wayGeoJson: any, partOf: OsmRelation[], splitPoints: any[]) {
|
||||
constructor(wayId: string, splitPointCoordinates: [number, number][]) {
|
||||
super()
|
||||
this.osmWay = osmWay;
|
||||
this.roadObject = wayGeoJson;
|
||||
this._partOf = partOf;
|
||||
this._splitPoints = splitPoints;
|
||||
this.wayId = wayId;
|
||||
this._splitPointsCoordinates = splitPointCoordinates
|
||||
}
|
||||
|
||||
private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] {
|
||||
|
@ -42,26 +38,17 @@ export default class SplitAction extends OsmChangeAction {
|
|||
return wayParts.filter(wp => wp.length > 0)
|
||||
}
|
||||
|
||||
CreateChangeDescriptions(changes: Changes): ChangeDescription[] {
|
||||
const splitPoints = this._splitPoints
|
||||
// We mark the new split points with a new id
|
||||
console.log(splitPoints)
|
||||
for (const splitPoint of splitPoints) {
|
||||
splitPoint.properties["_is_split_point"] = true
|
||||
}
|
||||
|
||||
|
||||
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
const self = this;
|
||||
const partOf = this._partOf
|
||||
const originalElement = this.osmWay
|
||||
const originalNodes = this.osmWay.nodes;
|
||||
const originalElement = <OsmWay>await OsmObject.DownloadObjectAsync(this.wayId)
|
||||
const originalNodes = originalElement.nodes;
|
||||
|
||||
// First, calculate splitpoints and remove points close to one another
|
||||
const splitInfo = self.CalculateSplitCoordinates(splitPoints)
|
||||
const splitInfo = self.CalculateSplitCoordinates(originalElement)
|
||||
// Now we have a list with e.g.
|
||||
// [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
|
||||
|
||||
// Lets change 'originalIndex' to the actual node id first:
|
||||
// Lets change 'originalIndex' to the actual node id first (or assign a new id if needed):
|
||||
for (const element of splitInfo) {
|
||||
if (element.originalIndex >= 0) {
|
||||
element.originalIndex = originalElement.nodes[element.originalIndex]
|
||||
|
@ -102,25 +89,30 @@ export default class SplitAction extends OsmChangeAction {
|
|||
})
|
||||
}
|
||||
|
||||
const newWayIds: number[] = []
|
||||
// The ids of all the ways (including the original)
|
||||
const allWayIdsInOrder: number[] = []
|
||||
|
||||
const allWaysNodesInOrder: number[][] = []
|
||||
// Lets create OsmWays based on them
|
||||
for (const wayPart of wayParts) {
|
||||
|
||||
let isOriginal = wayPart === longest
|
||||
if (isOriginal) {
|
||||
// We change the actual element!
|
||||
const nodeIds = wayPart.map(p => p.originalIndex)
|
||||
changeDescription.push({
|
||||
type: "way",
|
||||
id: originalElement.id,
|
||||
changes: {
|
||||
locations: wayPart.map(p => p.lngLat),
|
||||
nodes: wayPart.map(p => p.originalIndex)
|
||||
coordinates: wayPart.map(p => p.lngLat),
|
||||
nodes: nodeIds
|
||||
}
|
||||
})
|
||||
allWayIdsInOrder.push(originalElement.id)
|
||||
allWaysNodesInOrder.push(nodeIds)
|
||||
} else {
|
||||
let id = changes.getNewID();
|
||||
newWayIds.push(id)
|
||||
|
||||
// Copy the tags from the original object onto the new
|
||||
const kv = []
|
||||
for (const k in originalElement.tags) {
|
||||
if (!originalElement.tags.hasOwnProperty(k)) {
|
||||
|
@ -131,22 +123,35 @@ export default class SplitAction extends OsmChangeAction {
|
|||
}
|
||||
kv.push({k: k, v: originalElement.tags[k]})
|
||||
}
|
||||
const nodeIds = wayPart.map(p => p.originalIndex)
|
||||
changeDescription.push({
|
||||
type: "way",
|
||||
id: id,
|
||||
tags: kv,
|
||||
changes: {
|
||||
locations: wayPart.map(p => p.lngLat),
|
||||
nodes: wayPart.map(p => p.originalIndex)
|
||||
coordinates: wayPart.map(p => p.lngLat),
|
||||
nodes: nodeIds
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
allWayIdsInOrder.push(id)
|
||||
allWaysNodesInOrder.push(nodeIds)
|
||||
}
|
||||
}
|
||||
|
||||
// At last, we still have to check that we aren't part of a relation...
|
||||
// At least, the order of the ways is identical, so we can keep the same roles
|
||||
changeDescription.push(...new RelationSplitlHandler(partOf, newWayIds, originalNodes).CreateChangeDescriptions(changes))
|
||||
const relations = await OsmObject.DownloadReferencingRelations(this.wayId)
|
||||
for (const relation of relations) {
|
||||
const changDescrs = await new RelationSplitHandler({
|
||||
relation: relation,
|
||||
allWayIdsInOrder: allWayIdsInOrder,
|
||||
originalNodes: originalNodes,
|
||||
allWaysNodesInOrder: allWaysNodesInOrder,
|
||||
originalWayId: originalElement.id
|
||||
}).CreateChangeDescriptions(changes)
|
||||
changeDescription.push(...changDescrs)
|
||||
}
|
||||
|
||||
// And we have our objects!
|
||||
// Time to upload
|
||||
|
@ -158,75 +163,96 @@ export default class SplitAction extends OsmChangeAction {
|
|||
* Calculates the actual points to split
|
||||
* If another point is closer then ~5m, we reuse that point
|
||||
*/
|
||||
private CalculateSplitCoordinates(
|
||||
splitPoints: any[],
|
||||
toleranceInM = 5): SplitInfo[] {
|
||||
private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] {
|
||||
const wayGeoJson = osmWay.asGeoJson()
|
||||
// Should be [lon, lat][]
|
||||
const originalPoints = osmWay.coordinates.map(c => <[number, number]>c.reverse())
|
||||
const allPoints: {
|
||||
coordinates: [number, number],
|
||||
isSplitPoint: boolean,
|
||||
originalIndex?: number, // Original index
|
||||
dist: number, // Distance from the nearest point on the original line
|
||||
location: number // Distance from the start of the way
|
||||
}[] = this._splitPointsCoordinates.map(c => {
|
||||
// From the turf.js docs:
|
||||
// The properties object will contain three values:
|
||||
// - `index`: closest point was found on nth line part,
|
||||
// - `dist`: distance between pt and the closest point,
|
||||
// `location`: distance along the line between start and the closest point.
|
||||
let projected = GeoOperations.nearestPoint(wayGeoJson, c)
|
||||
return ({
|
||||
coordinates: c,
|
||||
isSplitPoint: true,
|
||||
dist: projected.properties.dist,
|
||||
location: projected.properties.location
|
||||
});
|
||||
})
|
||||
|
||||
const allPoints = [...splitPoints];
|
||||
// We have a bunch of coordinates here: [ [lat, lon], [lat, lon], ...] ...
|
||||
const originalPoints: [number, number][] = this.roadObject.geometry.coordinates
|
||||
// We project them onto the line (which should yield pretty much the same point
|
||||
// We have a bunch of coordinates here: [ [lon, lon], [lat, lon], ...] ...
|
||||
// We project them onto the line (which should yield pretty much the same point and add them to allPoints
|
||||
for (let i = 0; i < originalPoints.length; i++) {
|
||||
let originalPoint = originalPoints[i];
|
||||
let projected = GeoOperations.nearestPoint(this.roadObject, originalPoint)
|
||||
projected.properties["_is_split_point"] = false
|
||||
projected.properties["_original_index"] = i
|
||||
allPoints.push(projected)
|
||||
let projected = GeoOperations.nearestPoint(wayGeoJson, originalPoint)
|
||||
allPoints.push({
|
||||
coordinates: originalPoint,
|
||||
isSplitPoint: false,
|
||||
location: projected.properties.location,
|
||||
originalIndex: i,
|
||||
dist: projected.properties.dist
|
||||
})
|
||||
}
|
||||
// At this point, we have a list of both the split point and the old points, with some properties to discriminate between them
|
||||
// We sort this list so that the new points are at the same location
|
||||
allPoints.sort((a, b) => a.properties.location - b.properties.location)
|
||||
allPoints.sort((a, b) => a.location - b.location)
|
||||
|
||||
// When this is done, we check that no now point is too close to an already existing point and no very small segments get created
|
||||
|
||||
/* for (let i = allPoints.length - 1; i > 0; i--) {
|
||||
|
||||
const point = allPoints[i];
|
||||
if (point.properties._original_index !== undefined) {
|
||||
// This point is already in OSM - we have to keep it!
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i != allPoints.length - 1) {
|
||||
const prevPoint = allPoints[i + 1]
|
||||
const diff = Math.abs(point.properties.location - prevPoint.properties.location) * 1000
|
||||
if (diff <= toleranceInM) {
|
||||
// To close to the previous point! We delete this point...
|
||||
allPoints.splice(i, 1)
|
||||
// ... and mark the previous point as a split point
|
||||
prevPoint.properties._is_split_point = true
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (i > 0) {
|
||||
const nextPoint = allPoints[i - 1]
|
||||
const diff = Math.abs(point.properties.location - nextPoint.properties.location) * 1000
|
||||
if (diff <= toleranceInM) {
|
||||
// To close to the next point! We delete this point...
|
||||
allPoints.splice(i, 1)
|
||||
// ... and mark the next point as a split point
|
||||
nextPoint.properties._is_split_point = true
|
||||
// noinspection UnnecessaryContinueJS
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// We don't have to remove this point...
|
||||
}*/
|
||||
for (let i = allPoints.length - 2; i >= 1; i--) {
|
||||
// We 'merge' points with already existing nodes if they are close enough to avoid closeby elements
|
||||
|
||||
// Note the loop bounds: we skip the first two and last two elements:
|
||||
// The first and last element are always part of the original way and should be kept
|
||||
// Furthermore, we run in reverse order as we'll delete elements on the go
|
||||
|
||||
const point = allPoints[i]
|
||||
if (point.originalIndex !== undefined) {
|
||||
// We keep the original points
|
||||
continue
|
||||
}
|
||||
if (point.dist * 1000 >= toleranceInM) {
|
||||
// No need to remove this one
|
||||
continue
|
||||
}
|
||||
|
||||
// At this point, 'dist' told us the point is pretty close to an already existing point.
|
||||
// Lets see which (already existing) point is closer and mark it as splitpoint
|
||||
const nextPoint = allPoints[i + 1]
|
||||
const prevPoint = allPoints[i - 1]
|
||||
const distToNext = nextPoint.location - point.location
|
||||
const distToPrev = prevPoint.location - point.location
|
||||
let closest = nextPoint
|
||||
if (distToNext > distToPrev) {
|
||||
closest = prevPoint
|
||||
}
|
||||
|
||||
// Ok, we have a closest point!
|
||||
closest.isSplitPoint = true;
|
||||
allPoints.splice(i, 1)
|
||||
|
||||
}
|
||||
|
||||
const splitInfo: SplitInfo[] = []
|
||||
let nextId = -1
|
||||
let nextId = -1 // Note: these IDs are overwritten later on, no need to use a global counter here
|
||||
|
||||
for (const p of allPoints) {
|
||||
let index = p.properties._original_index
|
||||
let index = p.originalIndex
|
||||
if (index === undefined) {
|
||||
index = nextId;
|
||||
nextId--;
|
||||
}
|
||||
const splitInfoElement = {
|
||||
originalIndex: index,
|
||||
lngLat: p.geometry.coordinates,
|
||||
doSplit: p.properties._is_split_point
|
||||
lngLat: p.coordinates,
|
||||
doSplit: p.isSplitPoint
|
||||
}
|
||||
splitInfo.push(splitInfoElement)
|
||||
}
|
||||
|
|
|
@ -21,12 +21,15 @@ export class Changes {
|
|||
*/
|
||||
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
|
||||
|
||||
public readonly pendingChanges = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||
public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
|
||||
private readonly isUploading = new UIEventSource(false);
|
||||
|
||||
private readonly previouslyCreated: OsmObject[] = []
|
||||
|
||||
constructor() {
|
||||
// We keep track of all changes just as well
|
||||
this.allChanges.setData([...this.pendingChanges.data])
|
||||
}
|
||||
|
||||
private static createChangesetFor(csId: string,
|
||||
|
@ -146,10 +149,13 @@ export class Changes {
|
|||
}
|
||||
|
||||
public applyAction(action: OsmChangeAction) {
|
||||
const changes = action.Perform(this)
|
||||
console.log("Received changes:", changes)
|
||||
this.pendingChanges.data.push(...changes);
|
||||
this.pendingChanges.ping();
|
||||
action.Perform(this).then(changes => {
|
||||
console.log("Received changes:", changes)
|
||||
this.pendingChanges.data.push(...changes);
|
||||
this.pendingChanges.ping();
|
||||
this.allChanges.data.push(...changes)
|
||||
this.allChanges.ping()
|
||||
})
|
||||
}
|
||||
|
||||
private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {Utils} from "../../Utils";
|
||||
import * as polygon_features from "../../assets/polygon-features.json";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import {BBox} from "../GeoOperations";
|
||||
|
||||
|
||||
export abstract class OsmObject {
|
||||
|
@ -9,11 +10,12 @@ export abstract class OsmObject {
|
|||
protected static backendURL = OsmObject.defaultBackend;
|
||||
private static polygonFeatures = OsmObject.constructPolygonFeatures()
|
||||
private static objectCache = new Map<string, UIEventSource<OsmObject>>();
|
||||
private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>();
|
||||
private static referencingRelationsCache = new Map<string, UIEventSource<OsmRelation[]>>();
|
||||
private static historyCache = new Map<string, UIEventSource<OsmObject[]>>();
|
||||
type: string;
|
||||
id: number;
|
||||
/**
|
||||
* The OSM tags as simple object
|
||||
*/
|
||||
tags: {} = {};
|
||||
version: number;
|
||||
public changed: boolean = false;
|
||||
|
@ -37,7 +39,7 @@ export abstract class OsmObject {
|
|||
this.backendURL = url;
|
||||
}
|
||||
|
||||
static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
|
||||
public static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
|
||||
let src: UIEventSource<OsmObject>;
|
||||
if (OsmObject.objectCache.has(id)) {
|
||||
src = OsmObject.objectCache.get(id)
|
||||
|
@ -47,80 +49,62 @@ export abstract class OsmObject {
|
|||
return src;
|
||||
}
|
||||
} else {
|
||||
src = new UIEventSource<OsmObject>(undefined)
|
||||
src = UIEventSource.FromPromise(OsmObject.DownloadObjectAsync(id))
|
||||
}
|
||||
|
||||
OsmObject.objectCache.set(id, src);
|
||||
return src;
|
||||
}
|
||||
|
||||
static async DownloadObjectAsync(id: string): Promise<OsmObject> {
|
||||
const splitted = id.split("/");
|
||||
const type = splitted[0];
|
||||
const idN = Number(splitted[1]);
|
||||
if (idN < 0) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
OsmObject.objectCache.set(id, src);
|
||||
const newContinuation = (element: OsmObject) => {
|
||||
src.setData(element)
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case("node"):
|
||||
new OsmNode(idN).Download(newContinuation);
|
||||
break;
|
||||
return await new OsmNode(idN).Download();
|
||||
case("way"):
|
||||
new OsmWay(idN).Download(newContinuation);
|
||||
break;
|
||||
return await new OsmWay(idN).Download();
|
||||
case("relation"):
|
||||
new OsmRelation(idN).Download(newContinuation);
|
||||
break;
|
||||
return await new OsmRelation(idN).Download();
|
||||
default:
|
||||
throw "Invalid object type:" + type + id;
|
||||
throw ("Invalid object type:" + type + id);
|
||||
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Downloads the ways that are using this node.
|
||||
* Beware: their geometry will be incomplete!
|
||||
*/
|
||||
public static DownloadReferencingWays(id: string): UIEventSource<OsmWay[]> {
|
||||
if (OsmObject.referencingWaysCache.has(id)) {
|
||||
return OsmObject.referencingWaysCache.get(id);
|
||||
}
|
||||
const waysSrc = new UIEventSource<OsmWay[]>([])
|
||||
OsmObject.referencingWaysCache.set(id, waysSrc);
|
||||
Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/ways`)
|
||||
.then(data => {
|
||||
const ways = data.elements.map(wayInfo => {
|
||||
public static DownloadReferencingWays(id: string): Promise<OsmWay[]> {
|
||||
return Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/ways`).then(
|
||||
data => {
|
||||
return data.elements.map(wayInfo => {
|
||||
const way = new OsmWay(wayInfo.id)
|
||||
way.LoadData(wayInfo)
|
||||
return way
|
||||
})
|
||||
waysSrc.setData(ways)
|
||||
})
|
||||
return waysSrc;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the relations that are using this feature.
|
||||
* Beware: their geometry will be incomplete!
|
||||
*/
|
||||
public static DownloadReferencingRelations(id: string): UIEventSource<OsmRelation[]> {
|
||||
if (OsmObject.referencingRelationsCache.has(id)) {
|
||||
return OsmObject.referencingRelationsCache.get(id);
|
||||
}
|
||||
const relsSrc = new UIEventSource<OsmRelation[]>(undefined)
|
||||
OsmObject.referencingRelationsCache.set(id, relsSrc);
|
||||
Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`)
|
||||
.then(data => {
|
||||
const rels = data.elements.map(wayInfo => {
|
||||
const rel = new OsmRelation(wayInfo.id)
|
||||
rel.LoadData(wayInfo)
|
||||
rel.SaveExtraData(wayInfo)
|
||||
return rel
|
||||
})
|
||||
relsSrc.setData(rels)
|
||||
})
|
||||
return relsSrc;
|
||||
public static async DownloadReferencingRelations(id: string): Promise<OsmRelation[]> {
|
||||
const data = await Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`)
|
||||
return data.elements.map(wayInfo => {
|
||||
const rel = new OsmRelation(wayInfo.id)
|
||||
rel.LoadData(wayInfo)
|
||||
rel.SaveExtraData(wayInfo)
|
||||
return rel
|
||||
})
|
||||
}
|
||||
|
||||
public static DownloadHistory(id: string): UIEventSource<OsmObject []> {
|
||||
|
@ -158,18 +142,11 @@ export abstract class OsmObject {
|
|||
}
|
||||
|
||||
// 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 = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
|
||||
Utils.downloadJson(url).then(data => {
|
||||
const elements: any[] = data.elements;
|
||||
const objects = OsmObject.ParseObjects(elements)
|
||||
callback(objects);
|
||||
|
||||
})
|
||||
public static async LoadArea(bbox: BBox): Promise<OsmObject[]> {
|
||||
const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}`
|
||||
const data = await Utils.downloadJson(url)
|
||||
const elements: any[] = data.elements;
|
||||
return OsmObject.ParseObjects(elements);
|
||||
}
|
||||
|
||||
public static DownloadAll(neededIds, forceRefresh = true): UIEventSource<OsmObject[]> {
|
||||
|
@ -283,39 +260,34 @@ export abstract class OsmObject {
|
|||
return tags;
|
||||
}
|
||||
|
||||
Download(continuation: ((element: OsmObject, meta: OsmObjectMeta) => void)) {
|
||||
/**
|
||||
* Downloads the object, a full download for ways and relations
|
||||
* @constructor
|
||||
*/
|
||||
async Download(): Promise<OsmObject> {
|
||||
const self = this;
|
||||
const full = this.type !== "way" ? "" : "/full";
|
||||
const url = `${OsmObject.backendURL}api/0.6/${this.type}/${this.id}${full}`;
|
||||
Utils.downloadJson(url).then(data => {
|
||||
|
||||
return await Utils.downloadJson(url).then(data => {
|
||||
const element = data.elements.pop();
|
||||
|
||||
let nodes = []
|
||||
if (self.type === "way" && data.elements.length >= 0) {
|
||||
nodes = OsmObject.ParseObjects(data.elements)
|
||||
}
|
||||
|
||||
if (self.type === "rellation") {
|
||||
throw "We should save some extra data"
|
||||
}
|
||||
|
||||
self.LoadData(element)
|
||||
self.SaveExtraData(element, nodes);
|
||||
|
||||
const meta = {
|
||||
"_last_edit:contributor": element.user,
|
||||
"_last_edit:contributor:uid": element.uid,
|
||||
"_last_edit:changeset": element.changeset,
|
||||
"_last_edit:timestamp": new Date(element.timestamp),
|
||||
"_version_number": element.version
|
||||
}
|
||||
|
||||
if (OsmObject.backendURL !== OsmObject.defaultBackend) {
|
||||
self.tags["_backend"] = OsmObject.backendURL
|
||||
meta["_backend"] = OsmObject.backendURL;
|
||||
}
|
||||
|
||||
continuation(self, meta);
|
||||
return this;
|
||||
}
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
@ -389,18 +361,10 @@ export class OsmNode extends OsmObject {
|
|||
}
|
||||
}
|
||||
|
||||
export interface OsmObjectMeta {
|
||||
"_last_edit:contributor": string,
|
||||
"_last_edit:contributor:uid": number,
|
||||
"_last_edit:changeset": number,
|
||||
"_last_edit:timestamp": Date,
|
||||
"_version_number": number
|
||||
|
||||
}
|
||||
|
||||
export class OsmWay extends OsmObject {
|
||||
|
||||
nodes: number[];
|
||||
// The coordinates of the way, [lat, lon][]
|
||||
coordinates: [number, number][] = []
|
||||
lat: number;
|
||||
lon: number;
|
||||
|
@ -455,12 +419,16 @@ export class OsmWay extends OsmObject {
|
|||
}
|
||||
|
||||
public asGeoJson() {
|
||||
let coordinates : ([number, number][] | [number, number][][]) = this.coordinates.map(c => <[number, number]>c.reverse());
|
||||
if(this.isPolygon()){
|
||||
coordinates = [coordinates]
|
||||
}
|
||||
return {
|
||||
"type": "Feature",
|
||||
"properties": this.tags,
|
||||
"geometry": {
|
||||
"type": this.isPolygon() ? "Polygon" : "LineString",
|
||||
"coordinates": this.coordinates.map(c => [c[1], c[0]])
|
||||
"coordinates": coordinates
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -511,7 +479,7 @@ ${members}${tags} </relation>
|
|||
this.members = element.members;
|
||||
}
|
||||
|
||||
asGeoJson() {
|
||||
asGeoJson(): any {
|
||||
throw "Not Implemented"
|
||||
}
|
||||
}
|
|
@ -60,7 +60,12 @@ export class UIEventSource<T> {
|
|||
|
||||
run();
|
||||
return source;
|
||||
|
||||
}
|
||||
|
||||
public static FromPromise<T>(promise : Promise<T>): UIEventSource<T>{
|
||||
const src = new UIEventSource<T>(undefined)
|
||||
promise.then(d => src.setData(d))
|
||||
return src
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -191,6 +196,14 @@ export class UIEventSource<T> {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
addCallbackD(callback: (data: T) => void) {
|
||||
this.addCallback(data => {
|
||||
if (data !== undefined && data !== null) {
|
||||
return callback(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class UIEventSourceTools {
|
||||
|
|
|
@ -9,7 +9,7 @@ export default class Hash {
|
|||
public static hash: UIEventSource<string> = Hash.Get();
|
||||
|
||||
/**
|
||||
* Gets the current string, including the pound sign
|
||||
* Gets the current string, including the pound sign if there is any
|
||||
* @constructor
|
||||
*/
|
||||
public static Current(): string {
|
||||
|
|
|
@ -127,7 +127,6 @@ export class QueryParameters {
|
|||
parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data))
|
||||
}
|
||||
// Don't pollute the history every time a parameter changes
|
||||
|
||||
history.replaceState(null, "", "?" + parts.join("&") + Hash.Current());
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue