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
|
@ -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 = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue