forked from MapComplete/MapComplete
Reformat all files with prettier
This commit is contained in:
parent
e22d189376
commit
b541d3eab4
382 changed files with 50893 additions and 35566 deletions
|
@ -1,26 +1,30 @@
|
|||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import MetaTagging from "../../MetaTagging";
|
||||
import {ElementStorage} from "../../ElementStorage";
|
||||
import {ExtraFuncParams} from "../../ExtraFunctions";
|
||||
import FeaturePipeline from "../FeaturePipeline";
|
||||
import {BBox} from "../../BBox";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import MetaTagging from "../../MetaTagging"
|
||||
import { ElementStorage } from "../../ElementStorage"
|
||||
import { ExtraFuncParams } from "../../ExtraFunctions"
|
||||
import FeaturePipeline from "../FeaturePipeline"
|
||||
import { BBox } from "../../BBox"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
|
||||
/****
|
||||
* Concerned with the logic of updating the right layer at the right time
|
||||
*/
|
||||
class MetatagUpdater {
|
||||
public readonly neededLayerBboxes = new Map<string /*layerId*/, BBox>()
|
||||
private source: FeatureSourceForLayer & Tiled;
|
||||
private source: FeatureSourceForLayer & Tiled
|
||||
private readonly params: ExtraFuncParams
|
||||
private state: { allElements?: ElementStorage };
|
||||
private state: { allElements?: ElementStorage }
|
||||
|
||||
private readonly isDirty = new UIEventSource(false)
|
||||
|
||||
constructor(source: FeatureSourceForLayer & Tiled, state: { allElements?: ElementStorage }, featurePipeline: FeaturePipeline) {
|
||||
this.state = state;
|
||||
this.source = source;
|
||||
const self = this;
|
||||
constructor(
|
||||
source: FeatureSourceForLayer & Tiled,
|
||||
state: { allElements?: ElementStorage },
|
||||
featurePipeline: FeaturePipeline
|
||||
) {
|
||||
this.state = state
|
||||
this.source = source
|
||||
const self = this
|
||||
this.params = {
|
||||
getFeatureById(id) {
|
||||
return state.allElements.ContainingFeatures.get(id)
|
||||
|
@ -29,21 +33,20 @@ class MetatagUpdater {
|
|||
// We keep track of the BBOX that this source needs
|
||||
let oldBbox: BBox = self.neededLayerBboxes.get(layerId)
|
||||
if (oldBbox === undefined) {
|
||||
self.neededLayerBboxes.set(layerId, bbox);
|
||||
self.neededLayerBboxes.set(layerId, bbox)
|
||||
} else if (!bbox.isContainedIn(oldBbox)) {
|
||||
self.neededLayerBboxes.set(layerId, oldBbox.unionWith(bbox))
|
||||
}
|
||||
return featurePipeline.GetFeaturesWithin(layerId, bbox)
|
||||
},
|
||||
memberships: featurePipeline.relationTracker
|
||||
memberships: featurePipeline.relationTracker,
|
||||
}
|
||||
this.isDirty.stabilized(100).addCallback(dirty => {
|
||||
this.isDirty.stabilized(100).addCallback((dirty) => {
|
||||
if (dirty) {
|
||||
self.updateMetaTags()
|
||||
}
|
||||
})
|
||||
this.source.features.addCallbackAndRunD(_ => self.isDirty.setData(true))
|
||||
|
||||
this.source.features.addCallbackAndRunD((_) => self.isDirty.setData(true))
|
||||
}
|
||||
|
||||
public requestUpdate() {
|
||||
|
@ -57,56 +60,58 @@ class MetatagUpdater {
|
|||
this.isDirty.setData(false)
|
||||
return
|
||||
}
|
||||
MetaTagging.addMetatags(
|
||||
features,
|
||||
this.params,
|
||||
this.source.layer.layerDef,
|
||||
this.state)
|
||||
MetaTagging.addMetatags(features, this.params, this.source.layer.layerDef, this.state)
|
||||
this.isDirty.setData(false)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default class MetaTagRecalculator {
|
||||
private _state: {
|
||||
allElements?: ElementStorage
|
||||
};
|
||||
private _featurePipeline: FeaturePipeline;
|
||||
private readonly _alreadyRegistered: Set<FeatureSourceForLayer & Tiled> = new Set<FeatureSourceForLayer & Tiled>()
|
||||
}
|
||||
private _featurePipeline: FeaturePipeline
|
||||
private readonly _alreadyRegistered: Set<FeatureSourceForLayer & Tiled> = new Set<
|
||||
FeatureSourceForLayer & Tiled
|
||||
>()
|
||||
private readonly _notifiers: MetatagUpdater[] = []
|
||||
|
||||
/**
|
||||
* The meta tag recalculator receives tiles of layers via the 'registerSource'-function.
|
||||
* It keeps track of which sources have had their share calculated, and which should be re-updated if some other data is loaded
|
||||
*/
|
||||
constructor(state: { allElements?: ElementStorage, currentView: FeatureSourceForLayer & Tiled }, featurePipeline: FeaturePipeline) {
|
||||
this._featurePipeline = featurePipeline;
|
||||
this._state = state;
|
||||
|
||||
if(state.currentView !== undefined){
|
||||
const currentViewUpdater = new MetatagUpdater(state.currentView, this._state, this._featurePipeline)
|
||||
this._alreadyRegistered.add(state.currentView)
|
||||
this._notifiers.push(currentViewUpdater)
|
||||
state.currentView.features.addCallback(_ => {
|
||||
console.debug("Requesting an update for currentView")
|
||||
currentViewUpdater.updateMetaTags();
|
||||
})
|
||||
}
|
||||
constructor(
|
||||
state: { allElements?: ElementStorage; currentView: FeatureSourceForLayer & Tiled },
|
||||
featurePipeline: FeaturePipeline
|
||||
) {
|
||||
this._featurePipeline = featurePipeline
|
||||
this._state = state
|
||||
|
||||
if (state.currentView !== undefined) {
|
||||
const currentViewUpdater = new MetatagUpdater(
|
||||
state.currentView,
|
||||
this._state,
|
||||
this._featurePipeline
|
||||
)
|
||||
this._alreadyRegistered.add(state.currentView)
|
||||
this._notifiers.push(currentViewUpdater)
|
||||
state.currentView.features.addCallback((_) => {
|
||||
console.debug("Requesting an update for currentView")
|
||||
currentViewUpdater.updateMetaTags()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public registerSource(source: FeatureSourceForLayer & Tiled, recalculateOnEveryChange = false) {
|
||||
if (source === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (this._alreadyRegistered.has(source)) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this._alreadyRegistered.add(source)
|
||||
this._notifiers.push(new MetatagUpdater(source, this._state, this._featurePipeline))
|
||||
const self = this;
|
||||
source.features.addCallbackAndRunD(_ => {
|
||||
const self = this
|
||||
source.features.addCallbackAndRunD((_) => {
|
||||
const layerName = source.layer.layerDef.id
|
||||
for (const updater of self._notifiers) {
|
||||
const neededBbox = updater.neededLayerBboxes.get(layerName)
|
||||
|
@ -118,7 +123,5 @@ export default class MetaTagRecalculator {
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import FeatureSource from "../FeatureSource";
|
||||
import {Store} from "../../UIEventSource";
|
||||
import {ElementStorage} from "../../ElementStorage";
|
||||
import FeatureSource from "../FeatureSource"
|
||||
import { Store } from "../../UIEventSource"
|
||||
import { ElementStorage } from "../../ElementStorage"
|
||||
|
||||
/**
|
||||
* Makes sure that every feature is added to the ElementsStorage, so that the tags-eventsource can be retrieved
|
||||
*/
|
||||
export default class RegisteringAllFromFeatureSourceActor {
|
||||
public readonly features: Store<{ feature: any; freshness: Date }[]>;
|
||||
public readonly name;
|
||||
public readonly features: Store<{ feature: any; freshness: Date }[]>
|
||||
public readonly name
|
||||
|
||||
constructor(source: FeatureSource, allElements: ElementStorage) {
|
||||
this.features = source.features;
|
||||
this.name = "RegisteringSource of " + source.name;
|
||||
this.features.addCallbackAndRunD(features => {
|
||||
this.features = source.features
|
||||
this.name = "RegisteringSource of " + source.name
|
||||
this.features.addCallbackAndRunD((features) => {
|
||||
for (const feature of features) {
|
||||
allElements.addOrGetElement(feature.feature)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import FeatureSource, {Tiled} from "../FeatureSource";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {IdbLocalStorage} from "../../Web/IdbLocalStorage";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import {BBox} from "../../BBox";
|
||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import Loc from "../../../Models/Loc";
|
||||
import FeatureSource, { Tiled } from "../FeatureSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import { BBox } from "../../BBox"
|
||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import Loc from "../../../Models/Loc"
|
||||
|
||||
/***
|
||||
* Saves all the features that are passed in to localstorage, so they can be retrieved on the next run
|
||||
|
@ -15,20 +15,23 @@ import Loc from "../../../Models/Loc";
|
|||
*/
|
||||
export default class SaveTileToLocalStorageActor {
|
||||
private readonly visitedTiles: UIEventSource<Map<number, Date>>
|
||||
private readonly _layer: LayerConfig;
|
||||
private readonly _layer: LayerConfig
|
||||
private readonly _flayer: FilteredLayer
|
||||
private readonly initializeTime = new Date()
|
||||
|
||||
constructor(layer: FilteredLayer) {
|
||||
this._flayer = layer
|
||||
this._layer = layer.layerDef
|
||||
this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id,
|
||||
{defaultValue: new Map<number, Date>(),})
|
||||
this.visitedTiles.stabilized(100).addCallbackAndRunD(tiles => {
|
||||
this.visitedTiles = IdbLocalStorage.Get("visited_tiles_" + this._layer.id, {
|
||||
defaultValue: new Map<number, Date>(),
|
||||
})
|
||||
this.visitedTiles.stabilized(100).addCallbackAndRunD((tiles) => {
|
||||
for (const key of Array.from(tiles.keys())) {
|
||||
const tileFreshness = tiles.get(key)
|
||||
|
||||
const toOld = (this.initializeTime.getTime() - tileFreshness.getTime()) > 1000 * this._layer.maxAgeOfCache
|
||||
const toOld =
|
||||
this.initializeTime.getTime() - tileFreshness.getTime() >
|
||||
1000 * this._layer.maxAgeOfCache
|
||||
if (toOld) {
|
||||
// Purge this tile
|
||||
this.SetIdb(key, undefined)
|
||||
|
@ -37,27 +40,28 @@ export default class SaveTileToLocalStorageActor {
|
|||
}
|
||||
}
|
||||
this.visitedTiles.ping()
|
||||
return true;
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
public LoadTilesFromDisk(currentBounds: UIEventSource<BBox>, location: UIEventSource<Loc>,
|
||||
registerFreshness: (tileId: number, freshness: Date) => void,
|
||||
registerTile: ((src: FeatureSource & Tiled) => void)) {
|
||||
const self = this;
|
||||
public LoadTilesFromDisk(
|
||||
currentBounds: UIEventSource<BBox>,
|
||||
location: UIEventSource<Loc>,
|
||||
registerFreshness: (tileId: number, freshness: Date) => void,
|
||||
registerTile: (src: FeatureSource & Tiled) => void
|
||||
) {
|
||||
const self = this
|
||||
const loadedTiles = new Set<number>()
|
||||
this.visitedTiles.addCallbackD(tiles => {
|
||||
this.visitedTiles.addCallbackD((tiles) => {
|
||||
if (tiles.size === 0) {
|
||||
// We don't do anything yet as probably not yet loaded from disk
|
||||
// We'll unregister later on
|
||||
return;
|
||||
return
|
||||
}
|
||||
currentBounds.addCallbackAndRunD(bbox => {
|
||||
|
||||
currentBounds.addCallbackAndRunD((bbox) => {
|
||||
if (self._layer.minzoomVisible > location.data.zoom) {
|
||||
// Not enough zoom
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Iterate over all available keys in the local storage, check which are needed and fresh enough
|
||||
|
@ -71,32 +75,35 @@ export default class SaveTileToLocalStorageActor {
|
|||
registerFreshness(key, tileFreshness)
|
||||
const tileBbox = BBox.fromTileIndex(key)
|
||||
if (!bbox.overlapsWith(tileBbox)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (loadedTiles.has(key)) {
|
||||
// Already loaded earlier
|
||||
continue
|
||||
}
|
||||
loadedTiles.add(key)
|
||||
this.GetIdb(key).then((features: { feature: any, freshness: Date }[]) => {
|
||||
if(features === undefined){
|
||||
return;
|
||||
this.GetIdb(key).then((features: { feature: any; freshness: Date }[]) => {
|
||||
if (features === undefined) {
|
||||
return
|
||||
}
|
||||
console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk")
|
||||
const src = new SimpleFeatureSource(self._flayer, key, new UIEventSource<{ feature: any; freshness: Date }[]>(features))
|
||||
const src = new SimpleFeatureSource(
|
||||
self._flayer,
|
||||
key,
|
||||
new UIEventSource<{ feature: any; freshness: Date }[]>(features)
|
||||
)
|
||||
registerTile(src)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return true; // Remove the callback
|
||||
|
||||
return true // Remove the callback
|
||||
})
|
||||
}
|
||||
|
||||
public addTile(tile: FeatureSource & Tiled) {
|
||||
const self = this
|
||||
tile.features.addCallbackAndRunD(features => {
|
||||
tile.features.addCallbackAndRunD((features) => {
|
||||
const now = new Date()
|
||||
|
||||
if (features.length > 0) {
|
||||
|
@ -109,11 +116,10 @@ export default class SaveTileToLocalStorageActor {
|
|||
|
||||
public poison(lon: number, lat: number) {
|
||||
for (let z = 0; z < 25; z++) {
|
||||
const {x, y} = Tiles.embedded_tile(lat, lon, z)
|
||||
const { x, y } = Tiles.embedded_tile(lat, lon, z)
|
||||
const tileId = Tiles.tile_index(z, x, y)
|
||||
this.visitedTiles.data.delete(tileId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public MarkVisited(tileId: number, freshness: Date) {
|
||||
|
@ -125,11 +131,18 @@ export default class SaveTileToLocalStorageActor {
|
|||
try {
|
||||
IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data)
|
||||
} catch (e) {
|
||||
console.error("Could not save tile to indexed-db: ", e, "tileIndex is:", tileIndex, "for layer", this._layer.id)
|
||||
console.error(
|
||||
"Could not save tile to indexed-db: ",
|
||||
e,
|
||||
"tileIndex is:",
|
||||
tileIndex,
|
||||
"for layer",
|
||||
this._layer.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private GetIdb(tileIndex) {
|
||||
return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,33 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import FilteringFeatureSource from "./Sources/FilteringFeatureSource";
|
||||
import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "./FeatureSource";
|
||||
import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource";
|
||||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import {TileHierarchyTools} from "./TiledFeatureSource/TileHierarchy";
|
||||
import RememberingSource from "./Sources/RememberingSource";
|
||||
import OverpassFeatureSource from "../Actors/OverpassFeatureSource";
|
||||
import GeoJsonSource from "./Sources/GeoJsonSource";
|
||||
import Loc from "../../Models/Loc";
|
||||
import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor";
|
||||
import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor";
|
||||
import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource";
|
||||
import {TileHierarchyMerger} from "./TiledFeatureSource/TileHierarchyMerger";
|
||||
import RelationsTracker from "../Osm/RelationsTracker";
|
||||
import {NewGeometryFromChangesFeatureSource} from "./Sources/NewGeometryFromChangesFeatureSource";
|
||||
import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator";
|
||||
import {BBox} from "../BBox";
|
||||
import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource";
|
||||
import {Tiles} from "../../Models/TileRange";
|
||||
import TileFreshnessCalculator from "./TileFreshnessCalculator";
|
||||
import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource";
|
||||
import MapState from "../State/MapState";
|
||||
import {ElementStorage} from "../ElementStorage";
|
||||
import {OsmFeature} from "../../Models/OsmFeature";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {FilterState} from "../../Models/FilteredLayer";
|
||||
import {GeoOperations} from "../GeoOperations";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import FilteringFeatureSource from "./Sources/FilteringFeatureSource"
|
||||
import PerLayerFeatureSourceSplitter from "./PerLayerFeatureSourceSplitter"
|
||||
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "./FeatureSource"
|
||||
import TiledFeatureSource from "./TiledFeatureSource/TiledFeatureSource"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { TileHierarchyTools } from "./TiledFeatureSource/TileHierarchy"
|
||||
import RememberingSource from "./Sources/RememberingSource"
|
||||
import OverpassFeatureSource from "../Actors/OverpassFeatureSource"
|
||||
import GeoJsonSource from "./Sources/GeoJsonSource"
|
||||
import Loc from "../../Models/Loc"
|
||||
import RegisteringAllFromFeatureSourceActor from "./Actors/RegisteringAllFromFeatureSourceActor"
|
||||
import SaveTileToLocalStorageActor from "./Actors/SaveTileToLocalStorageActor"
|
||||
import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource"
|
||||
import { TileHierarchyMerger } from "./TiledFeatureSource/TileHierarchyMerger"
|
||||
import RelationsTracker from "../Osm/RelationsTracker"
|
||||
import { NewGeometryFromChangesFeatureSource } from "./Sources/NewGeometryFromChangesFeatureSource"
|
||||
import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator"
|
||||
import { BBox } from "../BBox"
|
||||
import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import TileFreshnessCalculator from "./TileFreshnessCalculator"
|
||||
import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import MapState from "../State/MapState"
|
||||
import { ElementStorage } from "../ElementStorage"
|
||||
import { OsmFeature } from "../../Models/OsmFeature"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { FilterState } from "../../Models/FilteredLayer"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
/**
|
||||
* The features pipeline ties together a myriad of various datasources:
|
||||
|
@ -42,12 +41,12 @@ import {Utils} from "../../Utils";
|
|||
*
|
||||
*/
|
||||
export default class FeaturePipeline {
|
||||
|
||||
public readonly sufficientlyZoomed: Store<boolean>;
|
||||
public readonly runningQuery: Store<boolean>;
|
||||
public readonly timeout: UIEventSource<number>;
|
||||
public readonly sufficientlyZoomed: Store<boolean>
|
||||
public readonly runningQuery: Store<boolean>
|
||||
public readonly timeout: UIEventSource<number>
|
||||
public readonly somethingLoaded: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly newDataLoadedSignal: UIEventSource<FeatureSource> = new UIEventSource<FeatureSource>(undefined)
|
||||
public readonly newDataLoadedSignal: UIEventSource<FeatureSource> =
|
||||
new UIEventSource<FeatureSource>(undefined)
|
||||
public readonly relationTracker: RelationsTracker
|
||||
/**
|
||||
* Keeps track of all raw OSM-nodes.
|
||||
|
@ -55,19 +54,19 @@ export default class FeaturePipeline {
|
|||
*/
|
||||
public readonly fullNodeDatabase?: FullNodeDatabaseSource
|
||||
private readonly overpassUpdater: OverpassFeatureSource
|
||||
private state: MapState;
|
||||
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>;
|
||||
private state: MapState
|
||||
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>
|
||||
/**
|
||||
* Keeps track of the age of the loaded data.
|
||||
* Has one freshness-Calculator for every layer
|
||||
* @private
|
||||
*/
|
||||
private readonly freshnesses = new Map<string, TileFreshnessCalculator>();
|
||||
private readonly oldestAllowedDate: Date;
|
||||
private readonly freshnesses = new Map<string, TileFreshnessCalculator>()
|
||||
private readonly oldestAllowedDate: Date
|
||||
private readonly osmSourceZoomLevel
|
||||
private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>()
|
||||
|
||||
private readonly newGeometryHandler : NewGeometryFromChangesFeatureSource;
|
||||
|
||||
private readonly newGeometryHandler: NewGeometryFromChangesFeatureSource
|
||||
|
||||
constructor(
|
||||
handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
|
||||
|
@ -77,33 +76,40 @@ export default class FeaturePipeline {
|
|||
handleRawFeatureSource: (source: FeatureSourceForLayer) => void
|
||||
}
|
||||
) {
|
||||
this.state = state;
|
||||
this.state = state
|
||||
|
||||
const self = this
|
||||
const expiryInSeconds = Math.min(...state.layoutToUse?.layers?.map(l => l.maxAgeOfCache) ?? [])
|
||||
this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds);
|
||||
this.osmSourceZoomLevel = state.osmApiTileSize.data;
|
||||
const useOsmApi = state.locationControl.map(l => l.zoom > (state.overpassMaxZoom.data ?? 12))
|
||||
const expiryInSeconds = Math.min(
|
||||
...(state.layoutToUse?.layers?.map((l) => l.maxAgeOfCache) ?? [])
|
||||
)
|
||||
this.oldestAllowedDate = new Date(new Date().getTime() - expiryInSeconds)
|
||||
this.osmSourceZoomLevel = state.osmApiTileSize.data
|
||||
const useOsmApi = state.locationControl.map(
|
||||
(l) => l.zoom > (state.overpassMaxZoom.data ?? 12)
|
||||
)
|
||||
this.relationTracker = new RelationsTracker()
|
||||
|
||||
state.changes.allChanges.addCallbackAndRun(allChanges => {
|
||||
allChanges.filter(ch => ch.id < 0 && ch.changes !== undefined)
|
||||
.map(ch => ch.changes)
|
||||
.filter(coor => coor["lat"] !== undefined && coor["lon"] !== undefined)
|
||||
.forEach(coor => {
|
||||
state.layoutToUse.layers.forEach(l => self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"]))
|
||||
state.changes.allChanges.addCallbackAndRun((allChanges) => {
|
||||
allChanges
|
||||
.filter((ch) => ch.id < 0 && ch.changes !== undefined)
|
||||
.map((ch) => ch.changes)
|
||||
.filter((coor) => coor["lat"] !== undefined && coor["lon"] !== undefined)
|
||||
.forEach((coor) => {
|
||||
state.layoutToUse.layers.forEach((l) =>
|
||||
self.localStorageSavers.get(l.id)?.poison(coor["lon"], coor["lat"])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
this.sufficientlyZoomed = state.locationControl.map(location => {
|
||||
if (location?.zoom === undefined) {
|
||||
return false;
|
||||
}
|
||||
let minzoom = Math.min(...state.filteredLayers.data.map(layer => layer.layerDef.minzoom ?? 18));
|
||||
return location.zoom >= minzoom;
|
||||
this.sufficientlyZoomed = state.locationControl.map((location) => {
|
||||
if (location?.zoom === undefined) {
|
||||
return false
|
||||
}
|
||||
);
|
||||
let minzoom = Math.min(
|
||||
...state.filteredLayers.data.map((layer) => layer.layerDef.minzoom ?? 18)
|
||||
)
|
||||
return location.zoom >= minzoom
|
||||
})
|
||||
|
||||
const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed)
|
||||
|
||||
|
@ -111,9 +117,11 @@ export default class FeaturePipeline {
|
|||
this.perLayerHierarchy = perLayerHierarchy
|
||||
|
||||
// Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource'
|
||||
function patchedHandleFeatureSource(src: FeatureSourceForLayer & IndexedFeatureSource & Tiled) {
|
||||
function patchedHandleFeatureSource(
|
||||
src: FeatureSourceForLayer & IndexedFeatureSource & Tiled
|
||||
) {
|
||||
// This will already contain the merged features for this tile. In other words, this will only be triggered once for every tile
|
||||
const withChanges = new ChangeGeometryApplicator(src, state.changes);
|
||||
const withChanges = new ChangeGeometryApplicator(src, state.changes)
|
||||
const srcFiltered = new FilteringFeatureSource(state, src.tileIndex, withChanges)
|
||||
|
||||
handleFeatureSource(srcFiltered)
|
||||
|
@ -127,31 +135,29 @@ export default class FeaturePipeline {
|
|||
function handlePriviligedFeatureSource(src: FeatureSourceForLayer & Tiled) {
|
||||
// Passthrough to passed function, except that it registers as well
|
||||
handleFeatureSource(src)
|
||||
src.features.addCallbackAndRunD(fs => {
|
||||
fs.forEach(ff => state.allElements.addOrGetElement(ff.feature))
|
||||
src.features.addCallbackAndRunD((fs) => {
|
||||
fs.forEach((ff) => state.allElements.addOrGetElement(ff.feature))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
for (const filteredLayer of state.filteredLayers.data) {
|
||||
const id = filteredLayer.layerDef.id
|
||||
const source = filteredLayer.layerDef.source
|
||||
|
||||
const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) => patchedHandleFeatureSource(tile))
|
||||
const hierarchy = new TileHierarchyMerger(filteredLayer, (tile, _) =>
|
||||
patchedHandleFeatureSource(tile)
|
||||
)
|
||||
perLayerHierarchy.set(id, hierarchy)
|
||||
|
||||
this.freshnesses.set(id, new TileFreshnessCalculator())
|
||||
|
||||
if (id === "type_node") {
|
||||
|
||||
this.fullNodeDatabase = new FullNodeDatabaseSource(
|
||||
filteredLayer,
|
||||
tile => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
|
||||
});
|
||||
continue;
|
||||
this.fullNodeDatabase = new FullNodeDatabaseSource(filteredLayer, (tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if (id === "gps_location") {
|
||||
|
@ -187,13 +193,15 @@ export default class FeaturePipeline {
|
|||
// We load the cached values and register them
|
||||
// Getting data from upstream happens a bit lower
|
||||
localTileSaver.LoadTilesFromDisk(
|
||||
state.currentBounds, state.locationControl,
|
||||
(tileIndex, freshness) => self.freshnesses.get(id).addTileLoad(tileIndex, freshness),
|
||||
state.currentBounds,
|
||||
state.locationControl,
|
||||
(tileIndex, freshness) =>
|
||||
self.freshnesses.get(id).addTileLoad(tileIndex, freshness),
|
||||
(tile) => {
|
||||
console.debug("Loaded tile ", id, tile.tileIndex, "from local cache")
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
hierarchy.registerTile(tile);
|
||||
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
|
||||
hierarchy.registerTile(tile)
|
||||
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -213,47 +221,48 @@ export default class FeaturePipeline {
|
|||
registerTile: (tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
perLayerHierarchy.get(id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
|
||||
}
|
||||
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
|
||||
},
|
||||
})
|
||||
} else {
|
||||
new RegisteringAllFromFeatureSourceActor(src, state.allElements)
|
||||
perLayerHierarchy.get(id).registerTile(src)
|
||||
src.features.addCallbackAndRunD(_ => self.onNewDataLoaded(src))
|
||||
src.features.addCallbackAndRunD((_) => self.onNewDataLoaded(src))
|
||||
}
|
||||
} else {
|
||||
new DynamicGeoJsonTileSource(
|
||||
filteredLayer,
|
||||
tile => {
|
||||
(tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
perLayerHierarchy.get(id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
|
||||
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
|
||||
},
|
||||
state
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const osmFeatureSource = new OsmFeatureSource({
|
||||
isActive: useOsmApi,
|
||||
neededTiles: neededTilesFromOsm,
|
||||
handleTile: tile => {
|
||||
handleTile: (tile) => {
|
||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||
if (tile.layer.layerDef.maxAgeOfCache > 0) {
|
||||
const saver = self.localStorageSavers.get(tile.layer.layerDef.id)
|
||||
if (saver === undefined) {
|
||||
console.error("No localStorageSaver found for layer ", tile.layer.layerDef.id)
|
||||
console.error(
|
||||
"No localStorageSaver found for layer ",
|
||||
tile.layer.layerDef.id
|
||||
)
|
||||
}
|
||||
saver?.addTile(tile)
|
||||
}
|
||||
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
||||
tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile))
|
||||
|
||||
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
|
||||
},
|
||||
state: state,
|
||||
markTileVisited: (tileId) =>
|
||||
state.filteredLayers.data.forEach(flayer => {
|
||||
state.filteredLayers.data.forEach((flayer) => {
|
||||
const layer = flayer.layerDef
|
||||
if (layer.maxAgeOfCache > 0) {
|
||||
const saver = self.localStorageSavers.get(layer.id)
|
||||
|
@ -264,110 +273,128 @@ export default class FeaturePipeline {
|
|||
}
|
||||
}
|
||||
self.freshnesses.get(layer.id).addTileLoad(tileId, new Date())
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
if (this.fullNodeDatabase !== undefined) {
|
||||
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => this.fullNodeDatabase.handleOsmJson(osmJson, tileId))
|
||||
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) =>
|
||||
this.fullNodeDatabase.handleOsmJson(osmJson, tileId)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const updater = this.initOverpassUpdater(state, useOsmApi)
|
||||
this.overpassUpdater = updater;
|
||||
this.overpassUpdater = updater
|
||||
this.timeout = updater.timeout
|
||||
|
||||
// Actually load data from the overpass source
|
||||
new PerLayerFeatureSourceSplitter(state.filteredLayers,
|
||||
(source) => TiledFeatureSource.createHierarchy(source, {
|
||||
layer: source.layer,
|
||||
minZoomLevel: source.layer.layerDef.minzoom,
|
||||
noDuplicates: true,
|
||||
maxFeatureCount: state.layoutToUse.clustering.minNeededElements,
|
||||
maxZoomLevel: state.layoutToUse.clustering.maxZoom,
|
||||
registerTile: (tile) => {
|
||||
// We save the tile data for the given layer to local storage - data sourced from overpass
|
||||
self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile)
|
||||
perLayerHierarchy.get(source.layer.layerDef.id).registerTile(new RememberingSource(tile))
|
||||
tile.features.addCallbackAndRunD(f => {
|
||||
if (f.length === 0) {
|
||||
return
|
||||
}
|
||||
self.onNewDataLoaded(tile)
|
||||
})
|
||||
|
||||
}
|
||||
}),
|
||||
new PerLayerFeatureSourceSplitter(
|
||||
state.filteredLayers,
|
||||
(source) =>
|
||||
TiledFeatureSource.createHierarchy(source, {
|
||||
layer: source.layer,
|
||||
minZoomLevel: source.layer.layerDef.minzoom,
|
||||
noDuplicates: true,
|
||||
maxFeatureCount: state.layoutToUse.clustering.minNeededElements,
|
||||
maxZoomLevel: state.layoutToUse.clustering.maxZoom,
|
||||
registerTile: (tile) => {
|
||||
// We save the tile data for the given layer to local storage - data sourced from overpass
|
||||
self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile)
|
||||
perLayerHierarchy
|
||||
.get(source.layer.layerDef.id)
|
||||
.registerTile(new RememberingSource(tile))
|
||||
tile.features.addCallbackAndRunD((f) => {
|
||||
if (f.length === 0) {
|
||||
return
|
||||
}
|
||||
self.onNewDataLoaded(tile)
|
||||
})
|
||||
},
|
||||
}),
|
||||
updater,
|
||||
{
|
||||
handleLeftovers: (leftOvers) => {
|
||||
console.warn("Overpass returned a few non-matched features:", leftOvers)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
// Also load points/lines that are newly added.
|
||||
const newGeometry = new NewGeometryFromChangesFeatureSource(state.changes, state.allElements, state.osmConnection._oauth_config.url)
|
||||
this.newGeometryHandler = newGeometry;
|
||||
newGeometry.features.addCallbackAndRun(geometries => {
|
||||
// Also load points/lines that are newly added.
|
||||
const newGeometry = new NewGeometryFromChangesFeatureSource(
|
||||
state.changes,
|
||||
state.allElements,
|
||||
state.osmConnection._oauth_config.url
|
||||
)
|
||||
this.newGeometryHandler = newGeometry
|
||||
newGeometry.features.addCallbackAndRun((geometries) => {
|
||||
console.debug("New geometries are:", geometries)
|
||||
})
|
||||
|
||||
new RegisteringAllFromFeatureSourceActor(newGeometry, state.allElements)
|
||||
// A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
|
||||
new PerLayerFeatureSourceSplitter(state.filteredLayers,
|
||||
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
|
||||
perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer)
|
||||
// AT last, we always apply the metatags whenever possible
|
||||
perLayer.features.addCallbackAndRunD(_ => {
|
||||
self.onNewDataLoaded(perLayer);
|
||||
perLayer.features.addCallbackAndRunD((_) => {
|
||||
self.onNewDataLoaded(perLayer)
|
||||
})
|
||||
|
||||
},
|
||||
newGeometry,
|
||||
{
|
||||
handleLeftovers: (leftOvers) => {
|
||||
console.warn("Got some leftovers from the filteredLayers: ", leftOvers)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
this.runningQuery = updater.runningQuery.map(
|
||||
overpass => {
|
||||
console.log("FeaturePipeline: runningQuery state changed: Overpass", overpass ? "is querying," : "is idle,",
|
||||
"osmFeatureSource is", osmFeatureSource.isRunning ? "is running and needs " + neededTilesFromOsm.data?.length + " tiles (already got " + osmFeatureSource.downloadedTiles.size + " tiles )" : "is idle")
|
||||
return overpass || osmFeatureSource.isRunning.data;
|
||||
}, [osmFeatureSource.isRunning]
|
||||
(overpass) => {
|
||||
console.log(
|
||||
"FeaturePipeline: runningQuery state changed: Overpass",
|
||||
overpass ? "is querying," : "is idle,",
|
||||
"osmFeatureSource is",
|
||||
osmFeatureSource.isRunning
|
||||
? "is running and needs " +
|
||||
neededTilesFromOsm.data?.length +
|
||||
" tiles (already got " +
|
||||
osmFeatureSource.downloadedTiles.size +
|
||||
" tiles )"
|
||||
: "is idle"
|
||||
)
|
||||
return overpass || osmFeatureSource.isRunning.data
|
||||
},
|
||||
[osmFeatureSource.isRunning]
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
public GetAllFeaturesWithin(bbox: BBox): OsmFeature[][] {
|
||||
const self = this
|
||||
const tiles: OsmFeature[][] = []
|
||||
Array.from(this.perLayerHierarchy.keys())
|
||||
.forEach(key => {
|
||||
const fetched : OsmFeature[][] = self.GetFeaturesWithin(key, bbox)
|
||||
tiles.push(...fetched);
|
||||
})
|
||||
return tiles;
|
||||
Array.from(this.perLayerHierarchy.keys()).forEach((key) => {
|
||||
const fetched: OsmFeature[][] = self.GetFeaturesWithin(key, bbox)
|
||||
tiles.push(...fetched)
|
||||
})
|
||||
return tiles
|
||||
}
|
||||
|
||||
public GetAllFeaturesAndMetaWithin(bbox: BBox, layerIdWhitelist?: Set<string>):
|
||||
{features: OsmFeature[], layer: string}[] {
|
||||
public GetAllFeaturesAndMetaWithin(
|
||||
bbox: BBox,
|
||||
layerIdWhitelist?: Set<string>
|
||||
): { features: OsmFeature[]; layer: string }[] {
|
||||
const self = this
|
||||
const tiles :{features: any[], layer: string}[]= []
|
||||
Array.from(this.perLayerHierarchy.keys())
|
||||
.forEach(key => {
|
||||
if(layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)){
|
||||
return;
|
||||
}
|
||||
return tiles.push({
|
||||
layer: key,
|
||||
features: [].concat(...self.GetFeaturesWithin(key, bbox))
|
||||
});
|
||||
const tiles: { features: any[]; layer: string }[] = []
|
||||
Array.from(this.perLayerHierarchy.keys()).forEach((key) => {
|
||||
if (layerIdWhitelist !== undefined && !layerIdWhitelist.has(key)) {
|
||||
return
|
||||
}
|
||||
return tiles.push({
|
||||
layer: key,
|
||||
features: [].concat(...self.GetFeaturesWithin(key, bbox)),
|
||||
})
|
||||
return tiles;
|
||||
})
|
||||
return tiles
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -380,16 +407,24 @@ export default class FeaturePipeline {
|
|||
}
|
||||
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;
|
||||
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))
|
||||
.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)
|
||||
})
|
||||
}
|
||||
|
@ -399,16 +434,16 @@ export default class FeaturePipeline {
|
|||
}
|
||||
|
||||
private freshnessForVisibleLayers(z: number, x: number, y: number): Date {
|
||||
let oldestDate = undefined;
|
||||
let oldestDate = undefined
|
||||
for (const flayer of this.state.filteredLayers.data) {
|
||||
if (!flayer.isDisplayed.data && !flayer.layerDef.forceLoad) {
|
||||
continue
|
||||
}
|
||||
if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
if (flayer.layerDef.maxAgeOfCache === 0) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
const freshnessCalc = this.freshnesses.get(flayer.layerDef.id)
|
||||
if (freshnessCalc === undefined) {
|
||||
|
@ -428,117 +463,136 @@ export default class FeaturePipeline {
|
|||
}
|
||||
|
||||
/*
|
||||
* Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM
|
||||
* */
|
||||
* Gives an UIEventSource containing the tileIndexes of the tiles that should be loaded from OSM
|
||||
* */
|
||||
private getNeededTilesFromOsm(isSufficientlyZoomed: Store<boolean>): Store<number[]> {
|
||||
const self = this
|
||||
return this.state.currentBounds.map(bbox => {
|
||||
if (bbox === undefined) {
|
||||
return []
|
||||
}
|
||||
if (!isSufficientlyZoomed.data) {
|
||||
return [];
|
||||
}
|
||||
const osmSourceZoomLevel = self.osmSourceZoomLevel
|
||||
const range = bbox.containingTileRange(osmSourceZoomLevel)
|
||||
const tileIndexes = []
|
||||
if (range.total >= 100) {
|
||||
// Too much tiles!
|
||||
return undefined
|
||||
}
|
||||
Tiles.MapRange(range, (x, y) => {
|
||||
const i = Tiles.tile_index(osmSourceZoomLevel, x, y);
|
||||
const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y)
|
||||
if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) {
|
||||
console.debug("Skipping tile", osmSourceZoomLevel, x, y, "as a decently fresh one is available")
|
||||
// The cached tiles contain decently fresh data
|
||||
return undefined;
|
||||
return this.state.currentBounds.map(
|
||||
(bbox) => {
|
||||
if (bbox === undefined) {
|
||||
return []
|
||||
}
|
||||
tileIndexes.push(i)
|
||||
})
|
||||
return tileIndexes
|
||||
}, [isSufficientlyZoomed])
|
||||
if (!isSufficientlyZoomed.data) {
|
||||
return []
|
||||
}
|
||||
const osmSourceZoomLevel = self.osmSourceZoomLevel
|
||||
const range = bbox.containingTileRange(osmSourceZoomLevel)
|
||||
const tileIndexes = []
|
||||
if (range.total >= 100) {
|
||||
// Too much tiles!
|
||||
return undefined
|
||||
}
|
||||
Tiles.MapRange(range, (x, y) => {
|
||||
const i = Tiles.tile_index(osmSourceZoomLevel, x, y)
|
||||
const oldestDate = self.freshnessForVisibleLayers(osmSourceZoomLevel, x, y)
|
||||
if (oldestDate !== undefined && oldestDate > this.oldestAllowedDate) {
|
||||
console.debug(
|
||||
"Skipping tile",
|
||||
osmSourceZoomLevel,
|
||||
x,
|
||||
y,
|
||||
"as a decently fresh one is available"
|
||||
)
|
||||
// The cached tiles contain decently fresh data
|
||||
return undefined
|
||||
}
|
||||
tileIndexes.push(i)
|
||||
})
|
||||
return tileIndexes
|
||||
},
|
||||
[isSufficientlyZoomed]
|
||||
)
|
||||
}
|
||||
|
||||
private initOverpassUpdater(state: {
|
||||
allElements: ElementStorage;
|
||||
layoutToUse: LayoutConfig,
|
||||
currentBounds: Store<BBox>,
|
||||
locationControl: Store<Loc>,
|
||||
readonly overpassUrl: Store<string[]>;
|
||||
readonly overpassTimeout: Store<number>;
|
||||
readonly overpassMaxZoom: Store<number>,
|
||||
}, useOsmApi: Store<boolean>): OverpassFeatureSource {
|
||||
const minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom))
|
||||
const overpassIsActive = state.currentBounds.map(bbox => {
|
||||
if (bbox === undefined) {
|
||||
console.debug("Disabling overpass source: no bbox")
|
||||
return false
|
||||
}
|
||||
let zoom = state.locationControl.data.zoom
|
||||
if (zoom < minzoom) {
|
||||
// We are zoomed out over the zoomlevel of any layer
|
||||
console.debug("Disabling overpass source: zoom < minzoom")
|
||||
return false;
|
||||
}
|
||||
|
||||
const range = bbox.containingTileRange(zoom)
|
||||
if (range.total >= 5000) {
|
||||
// Let's assume we don't have so much data cached
|
||||
return true
|
||||
}
|
||||
const self = this;
|
||||
const allFreshnesses = Tiles.MapRange(range, (x, y) => self.freshnessForVisibleLayers(zoom, x, y))
|
||||
return allFreshnesses.some(freshness => freshness === undefined || freshness < this.oldestAllowedDate)
|
||||
}, [state.locationControl])
|
||||
|
||||
const self = this;
|
||||
const updater = new OverpassFeatureSource(state,
|
||||
{
|
||||
padToTiles: state.locationControl.map(l => Math.min(15, l.zoom + 1)),
|
||||
relationTracker: this.relationTracker,
|
||||
isActive: useOsmApi.map(b => !b && overpassIsActive.data, [overpassIsActive]),
|
||||
freshnesses: this.freshnesses,
|
||||
onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => {
|
||||
Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => {
|
||||
const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y)
|
||||
downloadedLayers.forEach(layer => {
|
||||
self.freshnesses.get(layer.id).addTileLoad(tileIndex, date)
|
||||
self.localStorageSavers.get(layer.id)?.MarkVisited(tileIndex, date)
|
||||
})
|
||||
})
|
||||
|
||||
private initOverpassUpdater(
|
||||
state: {
|
||||
allElements: ElementStorage
|
||||
layoutToUse: LayoutConfig
|
||||
currentBounds: Store<BBox>
|
||||
locationControl: Store<Loc>
|
||||
readonly overpassUrl: Store<string[]>
|
||||
readonly overpassTimeout: Store<number>
|
||||
readonly overpassMaxZoom: Store<number>
|
||||
},
|
||||
useOsmApi: Store<boolean>
|
||||
): OverpassFeatureSource {
|
||||
const minzoom = Math.min(...state.layoutToUse.layers.map((layer) => layer.minzoom))
|
||||
const overpassIsActive = state.currentBounds.map(
|
||||
(bbox) => {
|
||||
if (bbox === undefined) {
|
||||
console.debug("Disabling overpass source: no bbox")
|
||||
return false
|
||||
}
|
||||
let zoom = state.locationControl.data.zoom
|
||||
if (zoom < minzoom) {
|
||||
// We are zoomed out over the zoomlevel of any layer
|
||||
console.debug("Disabling overpass source: zoom < minzoom")
|
||||
return false
|
||||
}
|
||||
});
|
||||
|
||||
const range = bbox.containingTileRange(zoom)
|
||||
if (range.total >= 5000) {
|
||||
// Let's assume we don't have so much data cached
|
||||
return true
|
||||
}
|
||||
const self = this
|
||||
const allFreshnesses = Tiles.MapRange(range, (x, y) =>
|
||||
self.freshnessForVisibleLayers(zoom, x, y)
|
||||
)
|
||||
return allFreshnesses.some(
|
||||
(freshness) => freshness === undefined || freshness < this.oldestAllowedDate
|
||||
)
|
||||
},
|
||||
[state.locationControl]
|
||||
)
|
||||
|
||||
const self = this
|
||||
const updater = new OverpassFeatureSource(state, {
|
||||
padToTiles: state.locationControl.map((l) => Math.min(15, l.zoom + 1)),
|
||||
relationTracker: this.relationTracker,
|
||||
isActive: useOsmApi.map((b) => !b && overpassIsActive.data, [overpassIsActive]),
|
||||
freshnesses: this.freshnesses,
|
||||
onBboxLoaded: (bbox, date, downloadedLayers, paddedToZoomLevel) => {
|
||||
Tiles.MapRange(bbox.containingTileRange(paddedToZoomLevel), (x, y) => {
|
||||
const tileIndex = Tiles.tile_index(paddedToZoomLevel, x, y)
|
||||
downloadedLayers.forEach((layer) => {
|
||||
self.freshnesses.get(layer.id).addTileLoad(tileIndex, date)
|
||||
self.localStorageSavers.get(layer.id)?.MarkVisited(tileIndex, date)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Register everything in the state' 'AllElements'
|
||||
new RegisteringAllFromFeatureSourceActor(updater, state.allElements)
|
||||
return updater;
|
||||
return updater
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters
|
||||
*/
|
||||
public getAllVisibleElementsWithmeta(bbox: BBox): { center: [number, number], element: OsmFeature, layer: LayerConfig }[] {
|
||||
public getAllVisibleElementsWithmeta(
|
||||
bbox: BBox
|
||||
): { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] {
|
||||
if (bbox === undefined) {
|
||||
console.warn("No bbox")
|
||||
return []
|
||||
}
|
||||
|
||||
const layers = Utils.toIdRecord(this.state.layoutToUse.layers)
|
||||
const elementsWithMeta: { features: OsmFeature[], layer: string }[] = this.GetAllFeaturesAndMetaWithin(bbox)
|
||||
const elementsWithMeta: { features: OsmFeature[]; layer: string }[] =
|
||||
this.GetAllFeaturesAndMetaWithin(bbox)
|
||||
|
||||
let elements: {center: [number, number], element: OsmFeature, layer: LayerConfig }[] = []
|
||||
let elements: { center: [number, number]; element: OsmFeature; layer: LayerConfig }[] = []
|
||||
let seenElements = new Set<string>()
|
||||
for (const elementsWithMetaElement of elementsWithMeta) {
|
||||
const layer = layers[elementsWithMetaElement.layer]
|
||||
if(layer.title === undefined){
|
||||
if (layer.title === undefined) {
|
||||
continue
|
||||
}
|
||||
const filtered = this.state.filteredLayers.data.find(fl => fl.layerDef == layer);
|
||||
const filtered = this.state.filteredLayers.data.find((fl) => fl.layerDef == layer)
|
||||
for (let i = 0; i < elementsWithMetaElement.features.length; i++) {
|
||||
const element = elementsWithMetaElement.features[i];
|
||||
const element = elementsWithMetaElement.features[i]
|
||||
if (!filtered.isDisplayed.data) {
|
||||
continue
|
||||
}
|
||||
|
@ -552,35 +606,38 @@ export default class FeaturePipeline {
|
|||
if (layer?.isShown !== undefined && !layer.isShown.matchesProperties(element)) {
|
||||
continue
|
||||
}
|
||||
const activeFilters: FilterState[] = Array.from(filtered.appliedFilters.data.values());
|
||||
if (!activeFilters.every(filter => filter?.currentFilter === undefined || filter?.currentFilter?.matchesProperties(element.properties))) {
|
||||
const activeFilters: FilterState[] = Array.from(
|
||||
filtered.appliedFilters.data.values()
|
||||
)
|
||||
if (
|
||||
!activeFilters.every(
|
||||
(filter) =>
|
||||
filter?.currentFilter === undefined ||
|
||||
filter?.currentFilter?.matchesProperties(element.properties)
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const center = GeoOperations.centerpointCoordinates(element);
|
||||
const center = GeoOperations.centerpointCoordinates(element)
|
||||
elements.push({
|
||||
element,
|
||||
center,
|
||||
layer: layers[elementsWithMetaElement.layer],
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return elements;
|
||||
return elements
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inject a new point
|
||||
* Inject a new point
|
||||
*/
|
||||
InjectNewPoint(geojson) {
|
||||
this.newGeometryHandler.features.data.push({
|
||||
feature: geojson,
|
||||
freshness: new Date()
|
||||
freshness: new Date(),
|
||||
})
|
||||
this.newGeometryHandler.features.ping();
|
||||
this.newGeometryHandler.features.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import {Store, UIEventSource} from "../UIEventSource";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {BBox} from "../BBox";
|
||||
import {Feature, Geometry} from "@turf/turf";
|
||||
import {OsmFeature} from "../../Models/OsmFeature";
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import { BBox } from "../BBox"
|
||||
import { Feature, Geometry } from "@turf/turf"
|
||||
import { OsmFeature } from "../../Models/OsmFeature"
|
||||
|
||||
export default interface FeatureSource {
|
||||
features: Store<{ feature: OsmFeature, freshness: Date }[]>;
|
||||
features: Store<{ feature: OsmFeature; freshness: Date }[]>
|
||||
/**
|
||||
* Mainly used for debuging
|
||||
*/
|
||||
name: string;
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface Tiled {
|
||||
tileIndex: number,
|
||||
tileIndex: number
|
||||
bbox: BBox
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "./FeatureSource";
|
||||
import {Store} from "../UIEventSource";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
|
||||
|
||||
import FeatureSource, { FeatureSourceForLayer, Tiled } from "./FeatureSource"
|
||||
import { Store } from "../UIEventSource"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import SimpleFeatureSource from "./Sources/SimpleFeatureSource"
|
||||
|
||||
/**
|
||||
* In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled)
|
||||
|
@ -10,30 +9,30 @@ import SimpleFeatureSource from "./Sources/SimpleFeatureSource";
|
|||
* In any case, this featureSource marks the objects with _matching_layer_id
|
||||
*/
|
||||
export default class PerLayerFeatureSourceSplitter {
|
||||
|
||||
constructor(layers: Store<FilteredLayer[]>,
|
||||
handleLayerData: (source: FeatureSourceForLayer & Tiled) => void,
|
||||
upstream: FeatureSource,
|
||||
options?: {
|
||||
tileIndex?: number,
|
||||
handleLeftovers?: (featuresWithoutLayer: any[]) => void
|
||||
}) {
|
||||
|
||||
constructor(
|
||||
layers: Store<FilteredLayer[]>,
|
||||
handleLayerData: (source: FeatureSourceForLayer & Tiled) => void,
|
||||
upstream: FeatureSource,
|
||||
options?: {
|
||||
tileIndex?: number
|
||||
handleLeftovers?: (featuresWithoutLayer: any[]) => void
|
||||
}
|
||||
) {
|
||||
const knownLayers = new Map<string, SimpleFeatureSource>()
|
||||
|
||||
function update() {
|
||||
const features = upstream.features?.data;
|
||||
const features = upstream.features?.data
|
||||
if (features === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (layers.data === undefined || layers.data.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// We try to figure out (for each feature) in which feature store it should be saved.
|
||||
// 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 featuresPerLayer = new Map<string, { feature; freshness }[]>()
|
||||
const noLayerFound = []
|
||||
|
||||
for (const layer of layers.data) {
|
||||
|
@ -41,19 +40,19 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
}
|
||||
|
||||
for (const f of features) {
|
||||
let foundALayer = false;
|
||||
let foundALayer = false
|
||||
for (const layer of layers.data) {
|
||||
if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) {
|
||||
// We have found our matching layer!
|
||||
featuresPerLayer.get(layer.layerDef.id).push(f)
|
||||
foundALayer = true;
|
||||
foundALayer = true
|
||||
if (!layer.layerDef.passAllFeatures) {
|
||||
// If not 'passAllFeatures', we are done for this feature
|
||||
break
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!foundALayer){
|
||||
if (!foundALayer) {
|
||||
noLayerFound.push(f)
|
||||
}
|
||||
}
|
||||
|
@ -61,11 +60,11 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
// At this point, we have our features per layer as a list
|
||||
// We assign them to the correct featureSources
|
||||
for (const layer of layers.data) {
|
||||
const id = layer.layerDef.id;
|
||||
const id = layer.layerDef.id
|
||||
const features = featuresPerLayer.get(id)
|
||||
if (features === undefined) {
|
||||
// No such features for this layer
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
let featureSource = knownLayers.get(id)
|
||||
|
@ -86,7 +85,7 @@ export default class PerLayerFeatureSourceSplitter {
|
|||
}
|
||||
}
|
||||
|
||||
layers.addCallback(_ => update())
|
||||
upstream.features.addCallbackAndRunD(_ => update())
|
||||
layers.addCallback((_) => update())
|
||||
upstream.features.addCallbackAndRunD((_) => update())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +1,52 @@
|
|||
/**
|
||||
* Applies geometry changes from 'Changes' onto every feature of a featureSource
|
||||
*/
|
||||
import {Changes} from "../../Osm/Changes";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import {FeatureSourceForLayer, IndexedFeatureSource} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {ChangeDescription, ChangeDescriptionTools} from "../../Osm/Actions/ChangeDescription";
|
||||
|
||||
import { Changes } from "../../Osm/Changes"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import { FeatureSourceForLayer, IndexedFeatureSource } from "../FeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription"
|
||||
|
||||
export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string;
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
||||
new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
public readonly name: string
|
||||
public readonly layer: FilteredLayer
|
||||
private readonly source: IndexedFeatureSource;
|
||||
private readonly changes: Changes;
|
||||
private readonly source: IndexedFeatureSource
|
||||
private readonly changes: Changes
|
||||
|
||||
constructor(source: (IndexedFeatureSource & FeatureSourceForLayer), changes: Changes) {
|
||||
this.source = source;
|
||||
this.changes = changes;
|
||||
constructor(source: IndexedFeatureSource & FeatureSourceForLayer, changes: Changes) {
|
||||
this.source = source
|
||||
this.changes = changes
|
||||
this.layer = source.layer
|
||||
|
||||
this.name = "ChangesApplied(" + source.name + ")"
|
||||
this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined)
|
||||
|
||||
const self = this;
|
||||
source.features.addCallbackAndRunD(_ => self.update())
|
||||
|
||||
changes.allChanges.addCallbackAndRunD(_ => self.update())
|
||||
const self = this
|
||||
source.features.addCallbackAndRunD((_) => self.update())
|
||||
|
||||
changes.allChanges.addCallbackAndRunD((_) => self.update())
|
||||
}
|
||||
|
||||
private update() {
|
||||
const upstreamFeatures = this.source.features.data
|
||||
const upstreamIds = this.source.containedIds.data
|
||||
const changesToApply = this.changes.allChanges.data
|
||||
?.filter(ch =>
|
||||
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)
|
||||
ch.id > 0
|
||||
)
|
||||
|
||||
if (changesToApply === undefined || changesToApply.length === 0) {
|
||||
// No changes to apply!
|
||||
// Pass the original feature and lets continue our day
|
||||
this.features.setData(upstreamFeatures);
|
||||
return;
|
||||
this.features.setData(upstreamFeatures)
|
||||
return
|
||||
}
|
||||
|
||||
const changesPerId = new Map<string, ChangeDescription[]>()
|
||||
|
@ -58,27 +58,32 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer {
|
|||
changesPerId.set(key, [ch])
|
||||
}
|
||||
}
|
||||
const newFeatures: { feature: any, freshness: Date }[] = []
|
||||
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;
|
||||
continue
|
||||
}
|
||||
|
||||
// Allright! We have a feature to rewrite!
|
||||
const copy = {
|
||||
...feature
|
||||
...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)
|
||||
console.log("Applying a geometry change onto:", feature,"The change is:", change,"which becomes:", copy)
|
||||
console.log(
|
||||
"Applying a geometry change onto:",
|
||||
feature,
|
||||
"The change is:",
|
||||
change,
|
||||
"which becomes:",
|
||||
copy
|
||||
)
|
||||
newFeatures.push(copy)
|
||||
}
|
||||
this.features.setData(newFeatures)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,99 +1,112 @@
|
|||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
|
||||
export default class FeatureSourceMerger implements FeatureSourceForLayer, Tiled, IndexedFeatureSource {
|
||||
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name;
|
||||
export default class FeatureSourceMerger
|
||||
implements FeatureSourceForLayer, Tiled, IndexedFeatureSource
|
||||
{
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<
|
||||
{ feature: any; freshness: Date }[]
|
||||
>([])
|
||||
public readonly name
|
||||
public readonly layer: FilteredLayer
|
||||
public readonly tileIndex: number;
|
||||
public readonly bbox: BBox;
|
||||
public readonly containedIds: UIEventSource<Set<string>> = new UIEventSource<Set<string>>(new Set())
|
||||
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()
|
||||
)
|
||||
private readonly _sources: UIEventSource<FeatureSource[]>
|
||||
/**
|
||||
* Merges features from different featureSources for a single layer
|
||||
* Uses the freshest feature available in the case multiple sources offer data with the same identifier
|
||||
*/
|
||||
constructor(layer: FilteredLayer, tileIndex: number, bbox: BBox, sources: UIEventSource<FeatureSource[]>) {
|
||||
this.tileIndex = tileIndex;
|
||||
this.bbox = bbox;
|
||||
this._sources = sources;
|
||||
this.layer = layer;
|
||||
this.name = "FeatureSourceMerger(" + layer.layerDef.id + ", " + Tiles.tile_from_index(tileIndex).join(",") + ")"
|
||||
const self = this;
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
tileIndex: number,
|
||||
bbox: BBox,
|
||||
sources: UIEventSource<FeatureSource[]>
|
||||
) {
|
||||
this.tileIndex = tileIndex
|
||||
this.bbox = bbox
|
||||
this._sources = sources
|
||||
this.layer = layer
|
||||
this.name =
|
||||
"FeatureSourceMerger(" +
|
||||
layer.layerDef.id +
|
||||
", " +
|
||||
Tiles.tile_from_index(tileIndex).join(",") +
|
||||
")"
|
||||
const self = this
|
||||
|
||||
const handledSources = new Set<FeatureSource>();
|
||||
const handledSources = new Set<FeatureSource>()
|
||||
|
||||
sources.addCallbackAndRunD(sources => {
|
||||
let newSourceRegistered = false;
|
||||
sources.addCallbackAndRunD((sources) => {
|
||||
let newSourceRegistered = false
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
let source = sources[i];
|
||||
let source = sources[i]
|
||||
if (handledSources.has(source)) {
|
||||
continue
|
||||
}
|
||||
handledSources.add(source)
|
||||
newSourceRegistered = true
|
||||
source.features.addCallback(() => {
|
||||
self.Update();
|
||||
});
|
||||
self.Update()
|
||||
})
|
||||
if (newSourceRegistered) {
|
||||
self.Update();
|
||||
self.Update()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private Update() {
|
||||
|
||||
let somethingChanged = false;
|
||||
const all: Map<string, { feature: any, freshness: Date }> = new Map<string, { feature: any; freshness: Date }>();
|
||||
let somethingChanged = false
|
||||
const all: Map<string, { feature: any; freshness: Date }> = new Map<
|
||||
string,
|
||||
{ feature: any; freshness: Date }
|
||||
>()
|
||||
// We seed the dictionary with the previously loaded features
|
||||
const oldValues = this.features.data ?? [];
|
||||
const oldValues = this.features.data ?? []
|
||||
for (const oldValue of oldValues) {
|
||||
all.set(oldValue.feature.id, oldValue)
|
||||
}
|
||||
|
||||
for (const source of this._sources.data) {
|
||||
if (source?.features?.data === undefined) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
for (const f of source.features.data) {
|
||||
const id = f.feature.properties.id;
|
||||
const id = f.feature.properties.id
|
||||
if (!all.has(id)) {
|
||||
// This is a new feature
|
||||
somethingChanged = true;
|
||||
all.set(id, f);
|
||||
continue;
|
||||
somethingChanged = true
|
||||
all.set(id, f)
|
||||
continue
|
||||
}
|
||||
|
||||
// This value has been seen already, either in a previous run or by a previous datasource
|
||||
// Let's figure out if something changed
|
||||
const oldV = all.get(id);
|
||||
const oldV = all.get(id)
|
||||
if (oldV.freshness < f.freshness) {
|
||||
// Jup, this feature is fresher
|
||||
all.set(id, f);
|
||||
somethingChanged = true;
|
||||
all.set(id, f)
|
||||
somethingChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!somethingChanged) {
|
||||
// We don't bother triggering an update
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const newList = [];
|
||||
const newList = []
|
||||
all.forEach((value, _) => {
|
||||
newList.push(value)
|
||||
})
|
||||
this.containedIds.setData(new Set(all.keys()))
|
||||
this.features.setData(newList);
|
||||
this.features.setData(newList)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,34 +1,35 @@
|
|||
import {Store, UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer, {FilterState} from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {BBox} from "../../BBox";
|
||||
import {ElementStorage} from "../../ElementStorage";
|
||||
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||
import {OsmFeature} from "../../../Models/OsmFeature";
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { BBox } from "../../BBox"
|
||||
import { ElementStorage } from "../../ElementStorage"
|
||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||
import { OsmFeature } from "../../../Models/OsmFeature"
|
||||
|
||||
export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled {
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
||||
new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name;
|
||||
public readonly layer: FilteredLayer;
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<
|
||||
{ feature: any; freshness: Date }[]
|
||||
>([])
|
||||
public readonly name
|
||||
public readonly layer: FilteredLayer
|
||||
public readonly tileIndex: number
|
||||
public readonly bbox: BBox
|
||||
private readonly upstream: FeatureSourceForLayer;
|
||||
private readonly upstream: FeatureSourceForLayer
|
||||
private readonly state: {
|
||||
locationControl: Store<{ zoom: number }>;
|
||||
selectedElement: Store<any>,
|
||||
globalFilters: Store<{ filter: FilterState }[]>,
|
||||
locationControl: Store<{ zoom: number }>
|
||||
selectedElement: Store<any>
|
||||
globalFilters: Store<{ filter: FilterState }[]>
|
||||
allElements: ElementStorage
|
||||
};
|
||||
private readonly _alreadyRegistered = new Set<UIEventSource<any>>();
|
||||
}
|
||||
private readonly _alreadyRegistered = new Set<UIEventSource<any>>()
|
||||
private readonly _is_dirty = new UIEventSource(false)
|
||||
private previousFeatureSet: Set<any> = undefined;
|
||||
private previousFeatureSet: Set<any> = undefined
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
locationControl: Store<{ zoom: number }>,
|
||||
selectedElement: Store<any>,
|
||||
allElements: ElementStorage,
|
||||
locationControl: Store<{ zoom: number }>
|
||||
selectedElement: Store<any>
|
||||
allElements: ElementStorage
|
||||
globalFilters: Store<{ filter: FilterState }[]>
|
||||
},
|
||||
tileIndex,
|
||||
|
@ -41,92 +42,95 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
this.upstream = upstream
|
||||
this.state = state
|
||||
|
||||
this.layer = upstream.layer;
|
||||
const layer = upstream.layer;
|
||||
const self = this;
|
||||
this.layer = upstream.layer
|
||||
const layer = upstream.layer
|
||||
const self = this
|
||||
upstream.features.addCallback(() => {
|
||||
self.update();
|
||||
});
|
||||
|
||||
|
||||
layer.appliedFilters.addCallback(_ => {
|
||||
self.update()
|
||||
})
|
||||
|
||||
this._is_dirty.stabilized(1000).addCallbackAndRunD(dirty => {
|
||||
layer.appliedFilters.addCallback((_) => {
|
||||
self.update()
|
||||
})
|
||||
|
||||
this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => {
|
||||
if (dirty) {
|
||||
self.update()
|
||||
}
|
||||
})
|
||||
|
||||
metataggingUpdated?.addCallback(_ => {
|
||||
metataggingUpdated?.addCallback((_) => {
|
||||
self._is_dirty.setData(true)
|
||||
})
|
||||
|
||||
state.globalFilters.addCallback(_ => {
|
||||
|
||||
state.globalFilters.addCallback((_) => {
|
||||
self.update()
|
||||
})
|
||||
|
||||
this.update();
|
||||
this.update()
|
||||
}
|
||||
|
||||
private update() {
|
||||
const self = this;
|
||||
const layer = this.upstream.layer;
|
||||
const features: { feature: OsmFeature; freshness: Date }[] = (this.upstream.features.data ?? []);
|
||||
const includedFeatureIds = new Set<string>();
|
||||
const globalFilters = self.state.globalFilters.data.map(f => f.filter);
|
||||
const self = this
|
||||
const layer = this.upstream.layer
|
||||
const features: { feature: OsmFeature; freshness: Date }[] =
|
||||
this.upstream.features.data ?? []
|
||||
const includedFeatureIds = new Set<string>()
|
||||
const globalFilters = self.state.globalFilters.data.map((f) => f.filter)
|
||||
const newFeatures = (features ?? []).filter((f) => {
|
||||
|
||||
self.registerCallback(f.feature)
|
||||
|
||||
const isShown: TagsFilter = layer.layerDef.isShown;
|
||||
const tags = f.feature.properties;
|
||||
if (isShown !== undefined && !isShown.matchesProperties(tags) ) {
|
||||
return false;
|
||||
const isShown: TagsFilter = layer.layerDef.isShown
|
||||
const tags = f.feature.properties
|
||||
if (isShown !== undefined && !isShown.matchesProperties(tags)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const tagsFilter = Array.from(layer.appliedFilters?.data?.values() ?? [])
|
||||
for (const filter of tagsFilter) {
|
||||
const neededTags: TagsFilter = filter?.currentFilter
|
||||
if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) {
|
||||
if (
|
||||
neededTags !== undefined &&
|
||||
!neededTags.matchesProperties(f.feature.properties)
|
||||
) {
|
||||
// Hidden by the filter on the layer itself - we want to hide it no matter what
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for (const filter of globalFilters) {
|
||||
const neededTags: TagsFilter = filter?.currentFilter
|
||||
if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) {
|
||||
if (
|
||||
neededTags !== undefined &&
|
||||
!neededTags.matchesProperties(f.feature.properties)
|
||||
) {
|
||||
// Hidden by the filter on the layer itself - we want to hide it no matter what
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
includedFeatureIds.add(f.feature.properties.id)
|
||||
return true;
|
||||
});
|
||||
return true
|
||||
})
|
||||
|
||||
const previousSet = this.previousFeatureSet;
|
||||
const previousSet = this.previousFeatureSet
|
||||
this._is_dirty.setData(false)
|
||||
|
||||
// Is there any difference between the two sets?
|
||||
if (previousSet !== undefined && previousSet.size === includedFeatureIds.size) {
|
||||
// The size of the sets is the same - they _might_ be identical
|
||||
const newItemFound = Array.from(includedFeatureIds).some(id => !previousSet.has(id))
|
||||
const newItemFound = Array.from(includedFeatureIds).some((id) => !previousSet.has(id))
|
||||
if (!newItemFound) {
|
||||
// We know that:
|
||||
// We know that:
|
||||
// - The sets have the same size
|
||||
// - Every item from the new set has been found in the old set
|
||||
// which means they are identical!
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Something new has been found!
|
||||
this.features.setData(newFeatures);
|
||||
|
||||
this.features.setData(newFeatures)
|
||||
}
|
||||
|
||||
private registerCallback(feature: any) {
|
||||
|
@ -139,11 +143,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
|||
}
|
||||
this._alreadyRegistered.add(src)
|
||||
|
||||
const self = this;
|
||||
const self = this
|
||||
// Add a callback as a changed tag migh change the filter
|
||||
src.addCallbackAndRunD(_ => {
|
||||
src.addCallbackAndRunD((_) => {
|
||||
self._is_dirty.setData(true)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,168 +1,163 @@
|
|||
/**
|
||||
* Fetches a geojson file somewhere and passes it along
|
||||
*/
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
|
||||
export default class GeoJsonSource implements FeatureSourceForLayer, Tiled {
|
||||
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
public readonly state = new UIEventSource<undefined | {error: string} | "loaded">(undefined)
|
||||
public readonly name;
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>
|
||||
public readonly state = new UIEventSource<undefined | { error: string } | "loaded">(undefined)
|
||||
public readonly name
|
||||
public readonly isOsmCache: boolean
|
||||
public readonly layer: FilteredLayer;
|
||||
public readonly layer: FilteredLayer
|
||||
public readonly tileIndex
|
||||
public readonly bbox;
|
||||
private readonly seenids: Set<string>;
|
||||
private readonly idKey ?: string;
|
||||
|
||||
public constructor(flayer: FilteredLayer,
|
||||
zxy?: [number, number, number] | BBox,
|
||||
options?: {
|
||||
featureIdBlacklist?: Set<string>
|
||||
}) {
|
||||
public readonly bbox
|
||||
private readonly seenids: Set<string>
|
||||
private readonly idKey?: string
|
||||
|
||||
public constructor(
|
||||
flayer: FilteredLayer,
|
||||
zxy?: [number, number, number] | BBox,
|
||||
options?: {
|
||||
featureIdBlacklist?: Set<string>
|
||||
}
|
||||
) {
|
||||
if (flayer.layerDef.source.geojsonZoomLevel !== undefined && zxy === undefined) {
|
||||
throw "Dynamic layers are not supported. Use 'DynamicGeoJsonTileSource instead"
|
||||
}
|
||||
|
||||
this.layer = flayer;
|
||||
this.layer = flayer
|
||||
this.idKey = flayer.layerDef.source.idKey
|
||||
this.seenids = options?.featureIdBlacklist ?? new Set<string>()
|
||||
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id);
|
||||
let url = flayer.layerDef.source.geojsonSource.replace("{layer}", flayer.layerDef.id)
|
||||
if (zxy !== undefined) {
|
||||
let tile_bbox: BBox;
|
||||
let tile_bbox: BBox
|
||||
if (zxy instanceof BBox) {
|
||||
tile_bbox = zxy;
|
||||
tile_bbox = zxy
|
||||
} else {
|
||||
const [z, x, y] = zxy;
|
||||
tile_bbox = BBox.fromTile(z, x, y);
|
||||
const [z, x, y] = zxy
|
||||
tile_bbox = BBox.fromTile(z, x, y)
|
||||
|
||||
this.tileIndex = Tiles.tile_index(z, x, y)
|
||||
this.bbox = BBox.fromTile(z, x, y)
|
||||
url = url
|
||||
.replace('{z}', "" + z)
|
||||
.replace('{x}', "" + x)
|
||||
.replace('{y}', "" + y)
|
||||
.replace("{z}", "" + z)
|
||||
.replace("{x}", "" + x)
|
||||
.replace("{y}", "" + y)
|
||||
}
|
||||
let bounds: { minLat: number, maxLat: number, minLon: number, maxLon: number } = tile_bbox
|
||||
let bounds: { minLat: number; maxLat: number; minLon: number; maxLon: number } =
|
||||
tile_bbox
|
||||
if (this.layer.layerDef.source.mercatorCrs) {
|
||||
bounds = tile_bbox.toMercator()
|
||||
}
|
||||
|
||||
url = url
|
||||
.replace('{y_min}', "" + bounds.minLat)
|
||||
.replace('{y_max}', "" + bounds.maxLat)
|
||||
.replace('{x_min}', "" + bounds.minLon)
|
||||
.replace('{x_max}', "" + bounds.maxLon)
|
||||
|
||||
|
||||
.replace("{y_min}", "" + bounds.minLat)
|
||||
.replace("{y_max}", "" + bounds.maxLat)
|
||||
.replace("{x_min}", "" + bounds.minLon)
|
||||
.replace("{x_max}", "" + bounds.maxLon)
|
||||
} else {
|
||||
this.tileIndex = Tiles.tile_index(0, 0, 0)
|
||||
this.bbox = BBox.global;
|
||||
this.bbox = BBox.global
|
||||
}
|
||||
|
||||
this.name = "GeoJsonSource of " + url;
|
||||
this.name = "GeoJsonSource of " + url
|
||||
|
||||
this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer;
|
||||
this.isOsmCache = flayer.layerDef.source.isOsmCacheLayer
|
||||
this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
this.LoadJSONFrom(url)
|
||||
}
|
||||
|
||||
|
||||
private LoadJSONFrom(url: string) {
|
||||
const eventSource = this.features;
|
||||
const self = this;
|
||||
const eventSource = this.features
|
||||
const self = this
|
||||
Utils.downloadJsonCached(url, 60 * 60)
|
||||
.then(json => {
|
||||
.then((json) => {
|
||||
self.state.setData("loaded")
|
||||
// TODO: move somewhere else, just for testing
|
||||
// Check for maproulette data
|
||||
if (url.startsWith("https://maproulette.org/api/v2/tasks/box/")) {
|
||||
console.log("MapRoulette data detected")
|
||||
const data = json;
|
||||
let maprouletteFeatures: any[] = [];
|
||||
data.forEach(element => {
|
||||
const data = json
|
||||
let maprouletteFeatures: any[] = []
|
||||
data.forEach((element) => {
|
||||
maprouletteFeatures.push({
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [element.point.lng, element.point.lat]
|
||||
coordinates: [element.point.lng, element.point.lat],
|
||||
},
|
||||
properties: {
|
||||
// Map all properties to the feature
|
||||
...element,
|
||||
}
|
||||
});
|
||||
});
|
||||
json.features = maprouletteFeatures;
|
||||
},
|
||||
})
|
||||
})
|
||||
json.features = maprouletteFeatures
|
||||
}
|
||||
|
||||
if (json.features === undefined || json.features === null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (self.layer.layerDef.source.mercatorCrs) {
|
||||
json = GeoOperations.GeoJsonToWGS84(json)
|
||||
}
|
||||
|
||||
const time = new Date();
|
||||
const newFeatures: { feature: any, freshness: Date } [] = []
|
||||
let i = 0;
|
||||
let skipped = 0;
|
||||
const time = new Date()
|
||||
const newFeatures: { feature: any; freshness: Date }[] = []
|
||||
let i = 0
|
||||
let skipped = 0
|
||||
for (const feature of json.features) {
|
||||
const props = feature.properties
|
||||
for (const key in props) {
|
||||
|
||||
if(props[key] === null){
|
||||
if (props[key] === null) {
|
||||
delete props[key]
|
||||
}
|
||||
|
||||
|
||||
if (typeof props[key] !== "string") {
|
||||
// Make sure all the values are string, it crashes stuff otherwise
|
||||
props[key] = JSON.stringify(props[key])
|
||||
}
|
||||
}
|
||||
|
||||
if(self.idKey !== undefined){
|
||||
if (self.idKey !== undefined) {
|
||||
props.id = props[self.idKey]
|
||||
}
|
||||
|
||||
|
||||
if (props.id === undefined) {
|
||||
props.id = url + "/" + i;
|
||||
feature.id = url + "/" + i;
|
||||
i++;
|
||||
props.id = url + "/" + i
|
||||
feature.id = url + "/" + i
|
||||
i++
|
||||
}
|
||||
if (self.seenids.has(props.id)) {
|
||||
skipped++;
|
||||
continue;
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
self.seenids.add(props.id)
|
||||
|
||||
let freshness: Date = time;
|
||||
let freshness: Date = time
|
||||
if (feature.properties["_last_edit:timestamp"] !== undefined) {
|
||||
freshness = new Date(props["_last_edit:timestamp"])
|
||||
}
|
||||
|
||||
newFeatures.push({feature: feature, freshness: freshness})
|
||||
newFeatures.push({ feature: feature, freshness: freshness })
|
||||
}
|
||||
|
||||
if (newFeatures.length == 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
eventSource.setData(eventSource.data.concat(newFeatures))
|
||||
|
||||
}).catch(msg => {
|
||||
console.debug("Could not load geojson layer", url, "due to", msg);
|
||||
self.state.setData({error: msg})
|
||||
})
|
||||
})
|
||||
.catch((msg) => {
|
||||
console.debug("Could not load geojson layer", url, "due to", msg)
|
||||
self.state.setData({ error: msg })
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,50 +1,50 @@
|
|||
import {Changes} from "../../Osm/Changes";
|
||||
import {OsmNode, OsmObject, OsmRelation, OsmWay} from "../../Osm/OsmObject";
|
||||
import FeatureSource from "../FeatureSource";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import {ChangeDescription} from "../../Osm/Actions/ChangeDescription";
|
||||
import {ElementStorage} from "../../ElementStorage";
|
||||
import {OsmId, OsmTags} from "../../../Models/OsmFeature";
|
||||
import { Changes } from "../../Osm/Changes"
|
||||
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject"
|
||||
import FeatureSource from "../FeatureSource"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import { ChangeDescription } from "../../Osm/Actions/ChangeDescription"
|
||||
import { ElementStorage } from "../../ElementStorage"
|
||||
import { OsmId, OsmTags } from "../../../Models/OsmFeature"
|
||||
|
||||
export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
||||
// This class name truly puts the 'Java' into 'Javascript'
|
||||
|
||||
/**
|
||||
* A feature source containing exclusively new elements.
|
||||
*
|
||||
*
|
||||
* These elements are probably created by the 'SimpleAddUi' which generates a new point, but the import functionality might create a line or polygon too.
|
||||
* Other sources of new points are e.g. imports from nodes
|
||||
*/
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
public readonly name: string = "newFeatures";
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
||||
new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
public readonly name: string = "newFeatures"
|
||||
|
||||
constructor(changes: Changes, allElementStorage: ElementStorage, backendUrl: string) {
|
||||
const seenChanges = new Set<ChangeDescription>()
|
||||
const features = this.features.data
|
||||
const self = this
|
||||
|
||||
const seenChanges = new Set<ChangeDescription>();
|
||||
const features = this.features.data;
|
||||
const self = this;
|
||||
|
||||
changes.pendingChanges.stabilized(100).addCallbackAndRunD(changes => {
|
||||
changes.pendingChanges.stabilized(100).addCallbackAndRunD((changes) => {
|
||||
if (changes.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let somethingChanged = false;
|
||||
const now = new Date()
|
||||
let somethingChanged = false
|
||||
|
||||
function add(feature) {
|
||||
feature.id = feature.properties.id
|
||||
features.push({
|
||||
feature: feature,
|
||||
freshness: now
|
||||
freshness: now,
|
||||
})
|
||||
somethingChanged = true;
|
||||
somethingChanged = true
|
||||
}
|
||||
|
||||
for (const change of changes) {
|
||||
if (seenChanges.has(change)) {
|
||||
// Already handled
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
seenChanges.add(change)
|
||||
|
||||
|
@ -60,35 +60,32 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
|||
// For this, we introspect the change
|
||||
if (allElementStorage.has(change.type + "/" + change.id)) {
|
||||
// The current point already exists, we don't have to do anything here
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
console.debug("Detected a reused point")
|
||||
// The 'allElementsStore' does _not_ have this point yet, so we have to create it
|
||||
OsmObject.DownloadObjectAsync(change.type + "/" + change.id).then(feat => {
|
||||
OsmObject.DownloadObjectAsync(change.type + "/" + change.id).then((feat) => {
|
||||
console.log("Got the reused point:", feat)
|
||||
for (const kv of change.tags) {
|
||||
feat.tags[kv.k] = kv.v
|
||||
}
|
||||
const geojson = feat.asGeoJson();
|
||||
const geojson = feat.asGeoJson()
|
||||
allElementStorage.addOrGetElement(geojson)
|
||||
self.features.data.push({feature: geojson, freshness: new Date()})
|
||||
self.features.data.push({ feature: geojson, freshness: new Date() })
|
||||
self.features.ping()
|
||||
})
|
||||
continue
|
||||
|
||||
|
||||
} else if (change.id < 0 && change.changes === undefined) {
|
||||
// The geometry is not described - not a new point
|
||||
if (change.id < 0) {
|
||||
console.error("WARNING: got a new point without geometry!")
|
||||
}
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const tags: OsmTags = {
|
||||
id: <OsmId> (change.type + "/" + change.id)
|
||||
id: <OsmId>(change.type + "/" + change.id),
|
||||
}
|
||||
for (const kv of change.tags) {
|
||||
tags[kv.k] = kv.v
|
||||
|
@ -104,30 +101,31 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
|||
n.lon = change.changes["lon"]
|
||||
const geojson = n.asGeoJson()
|
||||
add(geojson)
|
||||
break;
|
||||
break
|
||||
case "way":
|
||||
const w = new OsmWay(change.id)
|
||||
w.tags = tags
|
||||
w.nodes = change.changes["nodes"]
|
||||
w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [lat, lon])
|
||||
w.coordinates = change.changes["coordinates"].map(([lon, lat]) => [
|
||||
lat,
|
||||
lon,
|
||||
])
|
||||
add(w.asGeoJson())
|
||||
break;
|
||||
break
|
||||
case "relation":
|
||||
const r = new OsmRelation(change.id)
|
||||
r.tags = tags
|
||||
r.members = change.changes["members"]
|
||||
add(r.asGeoJson())
|
||||
break;
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not generate a new geometry to render on screen for:", e)
|
||||
}
|
||||
|
||||
}
|
||||
if (somethingChanged) {
|
||||
self.features.ping()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,34 +2,36 @@
|
|||
* Every previously added point is remembered, but new points are added.
|
||||
* Data coming from upstream will always overwrite a previous value
|
||||
*/
|
||||
import FeatureSource, {Tiled} from "../FeatureSource";
|
||||
import {Store, UIEventSource} from "../../UIEventSource";
|
||||
import {BBox} from "../../BBox";
|
||||
import FeatureSource, { Tiled } from "../FeatureSource"
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
export default class RememberingSource implements FeatureSource, Tiled {
|
||||
|
||||
public readonly features: Store<{ feature: any, freshness: Date }[]>;
|
||||
public readonly name;
|
||||
public readonly features: Store<{ feature: any; freshness: Date }[]>
|
||||
public readonly name
|
||||
public readonly tileIndex: number
|
||||
public readonly bbox: BBox
|
||||
|
||||
constructor(source: FeatureSource & Tiled) {
|
||||
const self = this;
|
||||
this.name = "RememberingSource of " + source.name;
|
||||
const self = this
|
||||
this.name = "RememberingSource of " + source.name
|
||||
this.tileIndex = source.tileIndex
|
||||
this.bbox = source.bbox;
|
||||
this.bbox = source.bbox
|
||||
|
||||
const empty = [];
|
||||
const featureSource = new UIEventSource<{feature: any, freshness: Date}[]>(empty)
|
||||
const empty = []
|
||||
const featureSource = new UIEventSource<{ feature: any; freshness: Date }[]>(empty)
|
||||
this.features = featureSource
|
||||
source.features.addCallbackAndRunD(features => {
|
||||
const oldFeatures = self.features?.data ?? empty;
|
||||
source.features.addCallbackAndRunD((features) => {
|
||||
const oldFeatures = self.features?.data ?? empty
|
||||
// Then new ids
|
||||
const ids = new Set<string>(features.map(f => f.feature.properties.id + f.feature.geometry.type));
|
||||
const ids = new Set<string>(
|
||||
features.map((f) => f.feature.properties.id + f.feature.geometry.type)
|
||||
)
|
||||
// the old data
|
||||
const oldData = oldFeatures.filter(old => !ids.has(old.feature.properties.id + old.feature.geometry.type))
|
||||
const oldData = oldFeatures.filter(
|
||||
(old) => !ids.has(old.feature.properties.id + old.feature.geometry.type)
|
||||
)
|
||||
featureSource.setData([...features, ...oldData])
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,57 @@
|
|||
/**
|
||||
* This feature source helps the ShowDataLayer class: it introduces the necessary extra features and indicates with what renderConfig it should be rendered.
|
||||
*/
|
||||
import {Store} from "../../UIEventSource";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
import FeatureSource from "../FeatureSource";
|
||||
import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig";
|
||||
import { Store } from "../../UIEventSource"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import FeatureSource from "../FeatureSource"
|
||||
import PointRenderingConfig from "../../../Models/ThemeConfig/PointRenderingConfig"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import LineRenderingConfig from "../../../Models/ThemeConfig/LineRenderingConfig"
|
||||
|
||||
export default class RenderingMultiPlexerFeatureSource {
|
||||
public readonly features: Store<(any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[]>;
|
||||
private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[];
|
||||
private centroidRenderings: { rendering: PointRenderingConfig; index: number }[];
|
||||
private projectedCentroidRenderings: { rendering: PointRenderingConfig; index: number }[];
|
||||
private startRenderings: { rendering: PointRenderingConfig; index: number }[];
|
||||
private endRenderings: { rendering: PointRenderingConfig; index: number }[];
|
||||
private hasCentroid: boolean;
|
||||
private lineRenderObjects: LineRenderingConfig[];
|
||||
public readonly features: Store<
|
||||
(any & {
|
||||
pointRenderingIndex: number | undefined
|
||||
lineRenderingIndex: number | undefined
|
||||
})[]
|
||||
>
|
||||
private readonly pointRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private centroidRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private projectedCentroidRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private startRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private endRenderings: { rendering: PointRenderingConfig; index: number }[]
|
||||
private hasCentroid: boolean
|
||||
private lineRenderObjects: LineRenderingConfig[]
|
||||
|
||||
|
||||
private inspectFeature(feat, addAsPoint: (feat, rendering, centerpoint: [number, number]) => void, withIndex: any[]){
|
||||
private inspectFeature(
|
||||
feat,
|
||||
addAsPoint: (feat, rendering, centerpoint: [number, number]) => void,
|
||||
withIndex: any[]
|
||||
) {
|
||||
if (feat.geometry.type === "Point") {
|
||||
|
||||
for (const rendering of this.pointRenderings) {
|
||||
withIndex.push({
|
||||
...feat,
|
||||
pointRenderingIndex: rendering.index
|
||||
pointRenderingIndex: rendering.index,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// This is a a line: add the centroids
|
||||
let centerpoint: [number, number] = undefined;
|
||||
let projectedCenterPoint : [number, number] = undefined
|
||||
if(this.hasCentroid){
|
||||
centerpoint = GeoOperations.centerpointCoordinates(feat)
|
||||
if(this.projectedCentroidRenderings.length > 0){
|
||||
projectedCenterPoint = <[number,number]> GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates
|
||||
let centerpoint: [number, number] = undefined
|
||||
let projectedCenterPoint: [number, number] = undefined
|
||||
if (this.hasCentroid) {
|
||||
centerpoint = GeoOperations.centerpointCoordinates(feat)
|
||||
if (this.projectedCentroidRenderings.length > 0) {
|
||||
projectedCenterPoint = <[number, number]>(
|
||||
GeoOperations.nearestPoint(feat, centerpoint).geometry.coordinates
|
||||
)
|
||||
}
|
||||
}
|
||||
for (const rendering of this.centroidRenderings) {
|
||||
addAsPoint(feat, rendering, centerpoint)
|
||||
}
|
||||
|
||||
|
||||
if (feat.geometry.type === "LineString") {
|
||||
|
||||
for (const rendering of this.projectedCentroidRenderings) {
|
||||
addAsPoint(feat, rendering, projectedCenterPoint)
|
||||
}
|
||||
|
@ -58,73 +65,69 @@ export default class RenderingMultiPlexerFeatureSource {
|
|||
const coordinate = coordinates[coordinates.length - 1]
|
||||
addAsPoint(feat, rendering, coordinate)
|
||||
}
|
||||
|
||||
}else{
|
||||
} else {
|
||||
for (const rendering of this.projectedCentroidRenderings) {
|
||||
addAsPoint(feat, rendering, centerpoint)
|
||||
}
|
||||
}
|
||||
|
||||
// AT last, add it 'as is' to what we should render
|
||||
// AT last, add it 'as is' to what we should render
|
||||
for (let i = 0; i < this.lineRenderObjects.length; i++) {
|
||||
withIndex.push({
|
||||
...feat,
|
||||
lineRenderingIndex: i
|
||||
lineRenderingIndex: i,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
constructor(upstream: FeatureSource, layer: LayerConfig) {
|
||||
|
||||
const pointRenderObjects: { rendering: PointRenderingConfig, index: number }[] = layer.mapRendering.map((r, i) => ({
|
||||
rendering: r,
|
||||
index: i
|
||||
}))
|
||||
this.pointRenderings = pointRenderObjects.filter(r => r.rendering.location.has("point"))
|
||||
this.centroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("centroid"))
|
||||
this.projectedCentroidRenderings = pointRenderObjects.filter(r => r.rendering.location.has("projected_centerpoint"))
|
||||
this.startRenderings = pointRenderObjects.filter(r => r.rendering.location.has("start"))
|
||||
this.endRenderings = pointRenderObjects.filter(r => r.rendering.location.has("end"))
|
||||
this.hasCentroid = this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0
|
||||
const pointRenderObjects: { rendering: PointRenderingConfig; index: number }[] =
|
||||
layer.mapRendering.map((r, i) => ({
|
||||
rendering: r,
|
||||
index: i,
|
||||
}))
|
||||
this.pointRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("point"))
|
||||
this.centroidRenderings = pointRenderObjects.filter((r) =>
|
||||
r.rendering.location.has("centroid")
|
||||
)
|
||||
this.projectedCentroidRenderings = pointRenderObjects.filter((r) =>
|
||||
r.rendering.location.has("projected_centerpoint")
|
||||
)
|
||||
this.startRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("start"))
|
||||
this.endRenderings = pointRenderObjects.filter((r) => r.rendering.location.has("end"))
|
||||
this.hasCentroid =
|
||||
this.centroidRenderings.length > 0 || this.projectedCentroidRenderings.length > 0
|
||||
this.lineRenderObjects = layer.lineRendering
|
||||
|
||||
this.features = upstream.features.map(
|
||||
features => {
|
||||
if (features === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
const withIndex: any[] = [];
|
||||
|
||||
function addAsPoint(feat, rendering, coordinate) {
|
||||
const patched = {
|
||||
...feat,
|
||||
pointRenderingIndex: rendering.index
|
||||
}
|
||||
patched.geometry = {
|
||||
type: "Point",
|
||||
coordinates: coordinate
|
||||
}
|
||||
withIndex.push(patched)
|
||||
}
|
||||
|
||||
|
||||
for (const f of features) {
|
||||
const feat = f.feature;
|
||||
if(feat === undefined){
|
||||
continue
|
||||
}
|
||||
this.inspectFeature(feat, addAsPoint, withIndex)
|
||||
}
|
||||
|
||||
|
||||
return withIndex;
|
||||
this.features = upstream.features.map((features) => {
|
||||
if (features === undefined) {
|
||||
return undefined
|
||||
}
|
||||
);
|
||||
|
||||
const withIndex: any[] = []
|
||||
|
||||
function addAsPoint(feat, rendering, coordinate) {
|
||||
const patched = {
|
||||
...feat,
|
||||
pointRenderingIndex: rendering.index,
|
||||
}
|
||||
patched.geometry = {
|
||||
type: "Point",
|
||||
coordinates: coordinate,
|
||||
}
|
||||
withIndex.push(patched)
|
||||
}
|
||||
|
||||
for (const f of features) {
|
||||
const feat = f.feature
|
||||
if (feat === undefined) {
|
||||
continue
|
||||
}
|
||||
this.inspectFeature(feat, addAsPoint, withIndex)
|
||||
}
|
||||
|
||||
return withIndex
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {BBox} from "../../BBox";
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
export default class SimpleFeatureSource implements FeatureSourceForLayer, Tiled {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
public readonly name: string = "SimpleFeatureSource";
|
||||
public readonly layer: FilteredLayer;
|
||||
public readonly bbox: BBox = BBox.global;
|
||||
public readonly tileIndex: number;
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>
|
||||
public readonly name: string = "SimpleFeatureSource"
|
||||
public readonly layer: FilteredLayer
|
||||
public readonly bbox: BBox = BBox.global
|
||||
public readonly tileIndex: number
|
||||
|
||||
constructor(layer: FilteredLayer, tileIndex: number, featureSource?: UIEventSource<{ feature: any; freshness: Date }[]> ) {
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
tileIndex: number,
|
||||
featureSource?: UIEventSource<{ feature: any; freshness: Date }[]>
|
||||
) {
|
||||
this.name = "SimpleFeatureSource(" + layer.layerDef.id + ")"
|
||||
this.layer = layer
|
||||
this.tileIndex = tileIndex ?? 0;
|
||||
this.tileIndex = tileIndex ?? 0
|
||||
this.bbox = BBox.fromTileIndex(this.tileIndex)
|
||||
this.features = featureSource ?? new UIEventSource<{ feature: any; freshness: Date }[]>([]);
|
||||
this.features = featureSource ?? new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,62 +1,90 @@
|
|||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {ImmutableStore, Store, UIEventSource} from "../../UIEventSource";
|
||||
import {stat} from "fs";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {BBox} from "../../BBox";
|
||||
import {Feature} from "@turf/turf";
|
||||
import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
|
||||
import { stat } from "fs"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { BBox } from "../../BBox"
|
||||
import { Feature } from "@turf/turf"
|
||||
|
||||
/**
|
||||
* A simple, read only feature store.
|
||||
*/
|
||||
export default class StaticFeatureSource implements FeatureSource {
|
||||
public readonly features: Store<{ feature: any; freshness: Date }[]>;
|
||||
public readonly features: Store<{ feature: any; freshness: Date }[]>
|
||||
public readonly name: string
|
||||
|
||||
constructor(features: Store<{ feature: Feature, freshness: Date }[]>, name = "StaticFeatureSource") {
|
||||
constructor(
|
||||
features: Store<{ feature: Feature; freshness: Date }[]>,
|
||||
name = "StaticFeatureSource"
|
||||
) {
|
||||
if (features === undefined) {
|
||||
throw "Static feature source received undefined as source"
|
||||
}
|
||||
this.name = name;
|
||||
this.features = features;
|
||||
this.name = name
|
||||
this.features = features
|
||||
}
|
||||
|
||||
public static fromGeojsonAndDate(features: { feature: Feature, freshness: Date }[], name = "StaticFeatureSourceFromGeojsonAndDate"): StaticFeatureSource {
|
||||
return new StaticFeatureSource(new ImmutableStore(features), name);
|
||||
public static fromGeojsonAndDate(
|
||||
features: { feature: Feature; freshness: Date }[],
|
||||
name = "StaticFeatureSourceFromGeojsonAndDate"
|
||||
): StaticFeatureSource {
|
||||
return new StaticFeatureSource(new ImmutableStore(features), name)
|
||||
}
|
||||
|
||||
|
||||
public static fromGeojson(geojson: Feature[], name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource {
|
||||
const now = new Date();
|
||||
return StaticFeatureSource.fromGeojsonAndDate(geojson.map(feature => ({feature, freshness: now})), name);
|
||||
public static fromGeojson(
|
||||
geojson: Feature[],
|
||||
name = "StaticFeatureSourceFromGeojson"
|
||||
): StaticFeatureSource {
|
||||
const now = new Date()
|
||||
return StaticFeatureSource.fromGeojsonAndDate(
|
||||
geojson.map((feature) => ({ feature, freshness: now })),
|
||||
name
|
||||
)
|
||||
}
|
||||
|
||||
public static fromGeojsonStore(geojson: Store<Feature[]>, name = "StaticFeatureSourceFromGeojson"): StaticFeatureSource {
|
||||
const now = new Date();
|
||||
const mapped : Store<{feature: Feature, freshness: Date}[]> = geojson.map(features => features.map(feature => ({feature, freshness: now})))
|
||||
return new StaticFeatureSource(mapped, name);
|
||||
public static fromGeojsonStore(
|
||||
geojson: Store<Feature[]>,
|
||||
name = "StaticFeatureSourceFromGeojson"
|
||||
): StaticFeatureSource {
|
||||
const now = new Date()
|
||||
const mapped: Store<{ feature: Feature; freshness: Date }[]> = geojson.map((features) =>
|
||||
features.map((feature) => ({ feature, freshness: now }))
|
||||
)
|
||||
return new StaticFeatureSource(mapped, name)
|
||||
}
|
||||
|
||||
static fromDateless(featureSource: Store<{ feature: Feature }[]>, name = "StaticFeatureSourceFromDateless") {
|
||||
const now = new Date();
|
||||
return new StaticFeatureSource(featureSource.map(features => features.map(feature => ({
|
||||
feature: feature.feature,
|
||||
freshness: now
|
||||
}))), name);
|
||||
static fromDateless(
|
||||
featureSource: Store<{ feature: Feature }[]>,
|
||||
name = "StaticFeatureSourceFromDateless"
|
||||
) {
|
||||
const now = new Date()
|
||||
return new StaticFeatureSource(
|
||||
featureSource.map((features) =>
|
||||
features.map((feature) => ({
|
||||
feature: feature.feature,
|
||||
freshness: now,
|
||||
}))
|
||||
),
|
||||
name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class TiledStaticFeatureSource extends StaticFeatureSource implements Tiled, FeatureSourceForLayer{
|
||||
export class TiledStaticFeatureSource
|
||||
extends StaticFeatureSource
|
||||
implements Tiled, FeatureSourceForLayer
|
||||
{
|
||||
public readonly bbox: BBox = BBox.global
|
||||
public readonly tileIndex: number
|
||||
public readonly layer: FilteredLayer
|
||||
|
||||
public readonly bbox: BBox = BBox.global;
|
||||
public readonly tileIndex: number;
|
||||
public readonly layer: FilteredLayer;
|
||||
|
||||
constructor(features: Store<{ feature: any, freshness: Date }[]>, layer: FilteredLayer ,tileIndex : number = 0) {
|
||||
super(features);
|
||||
this.tileIndex = tileIndex ;
|
||||
this.layer= layer;
|
||||
constructor(
|
||||
features: Store<{ feature: any; freshness: Date }[]>,
|
||||
layer: FilteredLayer,
|
||||
tileIndex: number = 0
|
||||
) {
|
||||
super(features)
|
||||
this.tileIndex = tileIndex
|
||||
this.layer = layer
|
||||
this.bbox = BBox.fromTileIndex(this.tileIndex)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import {Tiles} from "../../Models/TileRange";
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
|
||||
export default class TileFreshnessCalculator {
|
||||
|
||||
/**
|
||||
* All the freshnesses per tile index
|
||||
* @private
|
||||
*/
|
||||
private readonly freshnesses = new Map<number, Date>();
|
||||
private readonly freshnesses = new Map<number, Date>()
|
||||
|
||||
/**
|
||||
* Marks that some data got loaded for this layer
|
||||
|
@ -16,14 +15,14 @@ export default class TileFreshnessCalculator {
|
|||
public addTileLoad(tileId: number, freshness: Date) {
|
||||
const existingFreshness = this.freshnessFor(...Tiles.tile_from_index(tileId))
|
||||
if (existingFreshness >= freshness) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.freshnesses.set(tileId, freshness)
|
||||
|
||||
// Do we have freshness for the neighbouring tiles? If so, we can mark the tile above as loaded too!
|
||||
let [z, x, y] = Tiles.tile_from_index(tileId)
|
||||
if (z === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
x = x - (x % 2) // Make the tiles always even
|
||||
y = y - (y % 2)
|
||||
|
@ -48,11 +47,7 @@ export default class TileFreshnessCalculator {
|
|||
const leastFresh = Math.min(ul, ur, ll, lr)
|
||||
const date = new Date()
|
||||
date.setTime(leastFresh)
|
||||
this.addTileLoad(
|
||||
Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)),
|
||||
date
|
||||
)
|
||||
|
||||
this.addTileLoad(Tiles.tile_index(z - 1, Math.floor(x / 2), Math.floor(y / 2)), date)
|
||||
}
|
||||
|
||||
public freshnessFor(z: number, x: number, y: number): Date {
|
||||
|
@ -65,7 +60,5 @@ export default class TileFreshnessCalculator {
|
|||
}
|
||||
// recurse up
|
||||
return this.freshnessFor(z - 1, Math.floor(x / 2), Math.floor(y / 2))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,22 @@
|
|||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import DynamicTileSource from "./DynamicTileSource";
|
||||
import {Utils} from "../../../Utils";
|
||||
import GeoJsonSource from "../Sources/GeoJsonSource";
|
||||
import {BBox} from "../../BBox";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import DynamicTileSource from "./DynamicTileSource"
|
||||
import { Utils } from "../../../Utils"
|
||||
import GeoJsonSource from "../Sources/GeoJsonSource"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
||||
|
||||
private static whitelistCache = new Map<string, any>()
|
||||
|
||||
constructor(layer: FilteredLayer,
|
||||
registerLayer: (layer: FeatureSourceForLayer & Tiled) => void,
|
||||
state: {
|
||||
locationControl?: UIEventSource<{zoom?: number}>
|
||||
currentBounds: UIEventSource<BBox>
|
||||
}) {
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
registerLayer: (layer: FeatureSourceForLayer & Tiled) => void,
|
||||
state: {
|
||||
locationControl?: UIEventSource<{ zoom?: number }>
|
||||
currentBounds: UIEventSource<BBox>
|
||||
}
|
||||
) {
|
||||
const source = layer.layerDef.source
|
||||
if (source.geojsonZoomLevel === undefined) {
|
||||
throw "Invalid layer: geojsonZoomLevel expected"
|
||||
|
@ -26,7 +27,6 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
|||
|
||||
let whitelist = undefined
|
||||
if (source.geojsonSource.indexOf("{x}_{y}.geojson") > 0) {
|
||||
|
||||
const whitelistUrl = source.geojsonSource
|
||||
.replace("{z}", "" + source.geojsonZoomLevel)
|
||||
.replace("{x}_{y}.geojson", "overview.json")
|
||||
|
@ -35,26 +35,33 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
|||
if (DynamicGeoJsonTileSource.whitelistCache.has(whitelistUrl)) {
|
||||
whitelist = DynamicGeoJsonTileSource.whitelistCache.get(whitelistUrl)
|
||||
} else {
|
||||
Utils.downloadJsonCached(whitelistUrl, 1000 * 60 * 60).then(
|
||||
json => {
|
||||
const data = new Map<number, Set<number>>();
|
||||
Utils.downloadJsonCached(whitelistUrl, 1000 * 60 * 60)
|
||||
.then((json) => {
|
||||
const data = new Map<number, Set<number>>()
|
||||
for (const x in json) {
|
||||
if (x === "zoom") {
|
||||
continue
|
||||
}
|
||||
data.set(Number(x), new Set(json[x]))
|
||||
}
|
||||
console.log("The whitelist is", data, "based on ", json, "from", whitelistUrl)
|
||||
console.log(
|
||||
"The whitelist is",
|
||||
data,
|
||||
"based on ",
|
||||
json,
|
||||
"from",
|
||||
whitelistUrl
|
||||
)
|
||||
whitelist = data
|
||||
DynamicGeoJsonTileSource.whitelistCache.set(whitelistUrl, whitelist)
|
||||
}
|
||||
).catch(err => {
|
||||
console.warn("No whitelist found for ", layer.layerDef.id, err)
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("No whitelist found for ", layer.layerDef.id, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const blackList = (new Set<string>())
|
||||
const blackList = new Set<string>()
|
||||
super(
|
||||
layer,
|
||||
source.geojsonZoomLevel,
|
||||
|
@ -62,29 +69,28 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
|||
if (whitelist !== undefined) {
|
||||
const isWhiteListed = whitelist.get(zxy[1])?.has(zxy[2])
|
||||
if (!isWhiteListed) {
|
||||
console.debug("Not downloading tile", ...zxy, "as it is not on the whitelist")
|
||||
return undefined;
|
||||
console.debug(
|
||||
"Not downloading tile",
|
||||
...zxy,
|
||||
"as it is not on the whitelist"
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const src = new GeoJsonSource(
|
||||
layer,
|
||||
zxy,
|
||||
{
|
||||
featureIdBlacklist: blackList
|
||||
}
|
||||
)
|
||||
|
||||
const src = new GeoJsonSource(layer, zxy, {
|
||||
featureIdBlacklist: blackList,
|
||||
})
|
||||
|
||||
registerLayer(src)
|
||||
return src
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
public static RegisterWhitelist(url: string, json: any) {
|
||||
const data = new Map<number, Set<number>>();
|
||||
const data = new Map<number, Set<number>>()
|
||||
for (const x in json) {
|
||||
if (x === "zoom") {
|
||||
continue
|
||||
|
@ -93,5 +99,4 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
|||
}
|
||||
DynamicGeoJsonTileSource.whitelistCache.set(url, data)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,64 +1,80 @@
|
|||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import TileHierarchy from "./TileHierarchy";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import TileHierarchy from "./TileHierarchy"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
/***
|
||||
* A tiled source which dynamically loads the required tiles at a fixed zoom level
|
||||
*/
|
||||
export default class DynamicTileSource implements TileHierarchy<FeatureSourceForLayer & Tiled> {
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>;
|
||||
private readonly _loadedTiles = new Set<number>();
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled>
|
||||
private readonly _loadedTiles = new Set<number>()
|
||||
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
zoomlevel: number,
|
||||
constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled),
|
||||
constructTile: (zxy: [number, number, number]) => FeatureSourceForLayer & Tiled,
|
||||
state: {
|
||||
currentBounds: UIEventSource<BBox>;
|
||||
locationControl?: UIEventSource<{zoom?: number}>
|
||||
currentBounds: UIEventSource<BBox>
|
||||
locationControl?: UIEventSource<{ zoom?: number }>
|
||||
}
|
||||
) {
|
||||
const self = this;
|
||||
const self = this
|
||||
|
||||
this.loadedTiles = new Map<number, FeatureSourceForLayer & Tiled>()
|
||||
const neededTiles = state.currentBounds.map(
|
||||
bounds => {
|
||||
if (bounds === undefined) {
|
||||
// We'll retry later
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) {
|
||||
// No need to download! - the layer is disabled
|
||||
return undefined;
|
||||
}
|
||||
const neededTiles = state.currentBounds
|
||||
.map(
|
||||
(bounds) => {
|
||||
if (bounds === undefined) {
|
||||
// We'll retry later
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (state.locationControl?.data?.zoom !== undefined && state.locationControl.data.zoom < layer.layerDef.minzoom) {
|
||||
// No need to download! - the layer is disabled
|
||||
return undefined;
|
||||
}
|
||||
if (!layer.isDisplayed.data && !layer.layerDef.forceLoad) {
|
||||
// No need to download! - the layer is disabled
|
||||
return undefined
|
||||
}
|
||||
|
||||
const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest())
|
||||
if (tileRange.total > 10000) {
|
||||
console.error("Got a really big tilerange, bounds and location might be out of sync")
|
||||
return undefined
|
||||
}
|
||||
if (
|
||||
state.locationControl?.data?.zoom !== undefined &&
|
||||
state.locationControl.data.zoom < layer.layerDef.minzoom
|
||||
) {
|
||||
// No need to download! - the layer is disabled
|
||||
return undefined
|
||||
}
|
||||
|
||||
const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i))
|
||||
if (needed.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return needed
|
||||
}
|
||||
, [layer.isDisplayed, state.locationControl]).stabilized(250);
|
||||
const tileRange = Tiles.TileRangeBetween(
|
||||
zoomlevel,
|
||||
bounds.getNorth(),
|
||||
bounds.getEast(),
|
||||
bounds.getSouth(),
|
||||
bounds.getWest()
|
||||
)
|
||||
if (tileRange.total > 10000) {
|
||||
console.error(
|
||||
"Got a really big tilerange, bounds and location might be out of sync"
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
neededTiles.addCallbackAndRunD(neededIndexes => {
|
||||
const needed = Tiles.MapRange(tileRange, (x, y) =>
|
||||
Tiles.tile_index(zoomlevel, x, y)
|
||||
).filter((i) => !self._loadedTiles.has(i))
|
||||
if (needed.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return needed
|
||||
},
|
||||
[layer.isDisplayed, state.locationControl]
|
||||
)
|
||||
.stabilized(250)
|
||||
|
||||
neededTiles.addCallbackAndRunD((neededIndexes) => {
|
||||
console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes)
|
||||
if (neededIndexes === undefined) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
for (const neededIndex of neededIndexes) {
|
||||
self._loadedTiles.add(neededIndex)
|
||||
|
@ -68,10 +84,5 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +1,26 @@
|
|||
import TileHierarchy from "./TileHierarchy";
|
||||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject";
|
||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
|
||||
import TileHierarchy from "./TileHierarchy"
|
||||
import FeatureSource, { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject"
|
||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
|
||||
export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> {
|
||||
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
|
||||
private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void;
|
||||
private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void
|
||||
private readonly layer: FilteredLayer
|
||||
private readonly nodeByIds = new Map<number, OsmNode>();
|
||||
private readonly nodeByIds = new Map<number, OsmNode>()
|
||||
private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>()
|
||||
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
onTileLoaded: ((tile: Tiled & FeatureSourceForLayer) => void)) {
|
||||
constructor(layer: FilteredLayer, onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void) {
|
||||
this.onTileLoaded = onTileLoaded
|
||||
this.layer = layer;
|
||||
this.layer = layer
|
||||
if (this.layer === undefined) {
|
||||
throw "Layer is undefined"
|
||||
}
|
||||
}
|
||||
|
||||
public handleOsmJson(osmJson: any, tileId: number) {
|
||||
|
||||
const allObjects = OsmObject.ParseObjects(osmJson.elements)
|
||||
const nodesById = new Map<number, OsmNode>()
|
||||
|
||||
|
@ -32,7 +28,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
if (osmObj.type !== "node") {
|
||||
continue
|
||||
}
|
||||
const osmNode = <OsmNode>osmObj;
|
||||
const osmNode = <OsmNode>osmObj
|
||||
nodesById.set(osmNode.id, osmNode)
|
||||
this.nodeByIds.set(osmNode.id, osmNode)
|
||||
}
|
||||
|
@ -41,33 +37,32 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
if (osmObj.type !== "way") {
|
||||
continue
|
||||
}
|
||||
const osmWay = <OsmWay>osmObj;
|
||||
const osmWay = <OsmWay>osmObj
|
||||
for (const nodeId of osmWay.nodes) {
|
||||
|
||||
if (!this.parentWays.has(nodeId)) {
|
||||
const src = new UIEventSource<OsmWay[]>([])
|
||||
this.parentWays.set(nodeId, src)
|
||||
src.addCallback(parentWays => {
|
||||
src.addCallback((parentWays) => {
|
||||
const tgs = nodesById.get(nodeId).tags
|
||||
tgs ["parent_ways"] = JSON.stringify(parentWays.map(w => w.tags))
|
||||
tgs["parent_way_ids"] = JSON.stringify(parentWays.map(w => w.id))
|
||||
tgs["parent_ways"] = JSON.stringify(parentWays.map((w) => w.tags))
|
||||
tgs["parent_way_ids"] = JSON.stringify(parentWays.map((w) => w.id))
|
||||
})
|
||||
}
|
||||
const src = this.parentWays.get(nodeId)
|
||||
src.data.push(osmWay)
|
||||
src.ping();
|
||||
src.ping()
|
||||
}
|
||||
}
|
||||
const now = new Date()
|
||||
const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({
|
||||
feature: osmNode.asGeoJson(), freshness: now
|
||||
const asGeojsonFeatures = Array.from(nodesById.values()).map((osmNode) => ({
|
||||
feature: osmNode.asGeoJson(),
|
||||
freshness: now,
|
||||
}))
|
||||
|
||||
const featureSource = new SimpleFeatureSource(this.layer, tileId)
|
||||
featureSource.features.setData(asGeojsonFeatures)
|
||||
this.loadedTiles.set(tileId, featureSource)
|
||||
this.onTileLoaded(featureSource)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,6 +83,4 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
|||
public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> {
|
||||
return this.parentWays.get(nodeId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import {Utils} from "../../../Utils";
|
||||
import * as OsmToGeoJson from "osmtogeojson";
|
||||
import StaticFeatureSource from "../Sources/StaticFeatureSource";
|
||||
import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter";
|
||||
import {Store, UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Or} from "../../Tags/Or";
|
||||
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||
import {OsmObject} from "../../Osm/OsmObject";
|
||||
import {FeatureCollection} from "@turf/turf";
|
||||
import { Utils } from "../../../Utils"
|
||||
import * as OsmToGeoJson from "osmtogeojson"
|
||||
import StaticFeatureSource from "../Sources/StaticFeatureSource"
|
||||
import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter"
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Or } from "../../Tags/Or"
|
||||
import { TagsFilter } from "../../Tags/TagsFilter"
|
||||
import { OsmObject } from "../../Osm/OsmObject"
|
||||
import { FeatureCollection } from "@turf/turf"
|
||||
|
||||
/**
|
||||
* If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile'
|
||||
|
@ -20,67 +20,70 @@ export default class OsmFeatureSource {
|
|||
public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
public readonly downloadedTiles = new Set<number>()
|
||||
public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = []
|
||||
private readonly _backend: string;
|
||||
private readonly filteredLayers: Store<FilteredLayer[]>;
|
||||
private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void;
|
||||
private isActive: Store<boolean>;
|
||||
private readonly _backend: string
|
||||
private readonly filteredLayers: Store<FilteredLayer[]>
|
||||
private readonly handleTile: (fs: FeatureSourceForLayer & Tiled) => void
|
||||
private isActive: Store<boolean>
|
||||
private options: {
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
|
||||
isActive: Store<boolean>,
|
||||
neededTiles: Store<number[]>,
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void
|
||||
isActive: Store<boolean>
|
||||
neededTiles: Store<number[]>
|
||||
markTileVisited?: (tileId: number) => void
|
||||
};
|
||||
private readonly allowedTags: TagsFilter;
|
||||
}
|
||||
private readonly allowedTags: TagsFilter
|
||||
|
||||
/**
|
||||
*
|
||||
* @param options: allowedFeatures is normally calculated from the layoutToUse
|
||||
*/
|
||||
constructor(options: {
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void;
|
||||
isActive: Store<boolean>,
|
||||
neededTiles: Store<number[]>,
|
||||
handleTile: (tile: FeatureSourceForLayer & Tiled) => void
|
||||
isActive: Store<boolean>
|
||||
neededTiles: Store<number[]>
|
||||
state: {
|
||||
readonly filteredLayers: UIEventSource<FilteredLayer[]>;
|
||||
readonly filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
readonly osmConnection: {
|
||||
Backend(): string
|
||||
};
|
||||
}
|
||||
readonly layoutToUse?: LayoutConfig
|
||||
},
|
||||
readonly allowedFeatures?: TagsFilter,
|
||||
}
|
||||
readonly allowedFeatures?: TagsFilter
|
||||
markTileVisited?: (tileId: number) => void
|
||||
}) {
|
||||
this.options = options;
|
||||
this._backend = options.state.osmConnection.Backend();
|
||||
this.filteredLayers = options.state.filteredLayers.map(layers => layers.filter(layer => layer.layerDef.source.geojsonSource === undefined))
|
||||
this.options = options
|
||||
this._backend = options.state.osmConnection.Backend()
|
||||
this.filteredLayers = options.state.filteredLayers.map((layers) =>
|
||||
layers.filter((layer) => layer.layerDef.source.geojsonSource === undefined)
|
||||
)
|
||||
this.handleTile = options.handleTile
|
||||
this.isActive = options.isActive
|
||||
const self = this
|
||||
options.neededTiles.addCallbackAndRunD(neededTiles => {
|
||||
options.neededTiles.addCallbackAndRunD((neededTiles) => {
|
||||
self.Update(neededTiles)
|
||||
})
|
||||
|
||||
|
||||
const neededLayers = (options.state.layoutToUse?.layers ?? [])
|
||||
.filter(layer => !layer.doNotDownload)
|
||||
.filter(layer => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer)
|
||||
this.allowedTags = options.allowedFeatures ?? new Or(neededLayers.map(l => l.source.osmTags))
|
||||
.filter((layer) => !layer.doNotDownload)
|
||||
.filter(
|
||||
(layer) => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer
|
||||
)
|
||||
this.allowedTags =
|
||||
options.allowedFeatures ?? new Or(neededLayers.map((l) => l.source.osmTags))
|
||||
}
|
||||
|
||||
private async Update(neededTiles: number[]) {
|
||||
if (this.options.isActive?.data === false) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
neededTiles = neededTiles.filter(tile => !this.downloadedTiles.has(tile))
|
||||
neededTiles = neededTiles.filter((tile) => !this.downloadedTiles.has(tile))
|
||||
|
||||
if (neededTiles.length == 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
this.isRunning.setData(true)
|
||||
try {
|
||||
|
||||
for (const neededTile of neededTiles) {
|
||||
this.downloadedTiles.add(neededTile)
|
||||
await this.LoadTile(...Tiles.tile_from_index(neededTile))
|
||||
|
@ -98,24 +101,30 @@ export default class OsmFeatureSource {
|
|||
* This method will download the full relation and return it as geojson if it was incomplete.
|
||||
* If the feature is already complete (or is not a relation), the feature will be returned
|
||||
*/
|
||||
private async patchIncompleteRelations(feature: {properties: {id: string}},
|
||||
originalJson: {elements: {type: "node" | "way" | "relation", id: number, } []}): Promise<any> {
|
||||
if(!feature.properties.id.startsWith("relation")){
|
||||
private async patchIncompleteRelations(
|
||||
feature: { properties: { id: string } },
|
||||
originalJson: { elements: { type: "node" | "way" | "relation"; id: number }[] }
|
||||
): Promise<any> {
|
||||
if (!feature.properties.id.startsWith("relation")) {
|
||||
return feature
|
||||
}
|
||||
const relationSpec = originalJson.elements.find(f => "relation/"+f.id === feature.properties.id)
|
||||
const members : {type: string, ref: number}[] = relationSpec["members"]
|
||||
const relationSpec = originalJson.elements.find(
|
||||
(f) => "relation/" + f.id === feature.properties.id
|
||||
)
|
||||
const members: { type: string; ref: number }[] = relationSpec["members"]
|
||||
for (const member of members) {
|
||||
const isFound = originalJson.elements.some(f => f.id === member.ref && f.type === member.type)
|
||||
const isFound = originalJson.elements.some(
|
||||
(f) => f.id === member.ref && f.type === member.type
|
||||
)
|
||||
if (isFound) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// This member is missing. We redownload the entire relation instead
|
||||
console.debug("Fetching incomplete relation "+feature.properties.id)
|
||||
console.debug("Fetching incomplete relation " + feature.properties.id)
|
||||
return (await OsmObject.DownloadObjectAsync(feature.properties.id)).asGeoJson()
|
||||
}
|
||||
return feature;
|
||||
return feature
|
||||
}
|
||||
|
||||
private async LoadTile(z, x, y): Promise<void> {
|
||||
|
@ -130,52 +139,69 @@ export default class OsmFeatureSource {
|
|||
const bbox = BBox.fromTile(z, x, y)
|
||||
const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}`
|
||||
|
||||
let error = undefined;
|
||||
let error = undefined
|
||||
try {
|
||||
const osmJson = await Utils.downloadJson(url)
|
||||
try {
|
||||
|
||||
console.log("Got tile", z, x, y, "from the osm api")
|
||||
this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y)))
|
||||
const geojson = <FeatureCollection<any , {id: string}>> OsmToGeoJson.default(osmJson,
|
||||
this.rawDataHandlers.forEach((handler) =>
|
||||
handler(osmJson, Tiles.tile_index(z, x, y))
|
||||
)
|
||||
const geojson = <FeatureCollection<any, { id: string }>>OsmToGeoJson.default(
|
||||
osmJson,
|
||||
// @ts-ignore
|
||||
{
|
||||
flatProperties: true
|
||||
});
|
||||
|
||||
flatProperties: true,
|
||||
}
|
||||
)
|
||||
|
||||
// The geojson contains _all_ features at the given location
|
||||
// We only keep what is needed
|
||||
|
||||
geojson.features = geojson.features.filter(feature => this.allowedTags.matchesProperties(feature.properties))
|
||||
geojson.features = geojson.features.filter((feature) =>
|
||||
this.allowedTags.matchesProperties(feature.properties)
|
||||
)
|
||||
|
||||
for (let i = 0; i < geojson.features.length; i++) {
|
||||
geojson.features[i] = await this.patchIncompleteRelations(geojson.features[i], osmJson)
|
||||
geojson.features[i] = await this.patchIncompleteRelations(
|
||||
geojson.features[i],
|
||||
osmJson
|
||||
)
|
||||
}
|
||||
geojson.features.forEach(f => {
|
||||
geojson.features.forEach((f) => {
|
||||
f.properties["_backend"] = this._backend
|
||||
})
|
||||
|
||||
const index = Tiles.tile_index(z, x, y);
|
||||
new PerLayerFeatureSourceSplitter(this.filteredLayers,
|
||||
const index = Tiles.tile_index(z, x, y)
|
||||
new PerLayerFeatureSourceSplitter(
|
||||
this.filteredLayers,
|
||||
this.handleTile,
|
||||
StaticFeatureSource.fromGeojson(geojson.features),
|
||||
{
|
||||
tileIndex: index
|
||||
tileIndex: index,
|
||||
}
|
||||
);
|
||||
)
|
||||
if (this.options.markTileVisited) {
|
||||
this.options.markTileVisited(index)
|
||||
}
|
||||
}catch(e){
|
||||
console.error("PANIC: got the tile from the OSM-api, but something crashed handling this tile")
|
||||
error = e;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"PANIC: got the tile from the OSM-api, but something crashed handling this tile"
|
||||
)
|
||||
error = e
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error("Could not download tile", z, x, y, "due to", e, "; retrying with smaller bounds")
|
||||
console.error(
|
||||
"Could not download tile",
|
||||
z,
|
||||
x,
|
||||
y,
|
||||
"due to",
|
||||
e,
|
||||
"; retrying with smaller bounds"
|
||||
)
|
||||
if (e === "rate limited") {
|
||||
return;
|
||||
return
|
||||
}
|
||||
await this.LoadTile(z + 1, x * 2, y * 2)
|
||||
await this.LoadTile(z + 1, 1 + x * 2, y * 2)
|
||||
|
@ -183,10 +209,8 @@ export default class OsmFeatureSource {
|
|||
await this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2)
|
||||
}
|
||||
|
||||
if(error !== undefined){
|
||||
throw error;
|
||||
if (error !== undefined) {
|
||||
throw error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import FeatureSource, {Tiled} from "../FeatureSource";
|
||||
import {BBox} from "../../BBox";
|
||||
import FeatureSource, { Tiled } from "../FeatureSource"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
export default interface TileHierarchy<T extends FeatureSource & Tiled> {
|
||||
|
||||
/**
|
||||
* A mapping from 'tile_index' to the actual tile featrues
|
||||
*/
|
||||
loadedTiles: Map<number, T>
|
||||
|
||||
}
|
||||
|
||||
export class TileHierarchyTools {
|
||||
|
||||
public static getTiles<T extends FeatureSource & Tiled>(hierarchy: TileHierarchy<T>, bbox: BBox): T[] {
|
||||
public static getTiles<T extends FeatureSource & Tiled>(
|
||||
hierarchy: TileHierarchy<T>,
|
||||
bbox: BBox
|
||||
): T[] {
|
||||
const result: T[] = []
|
||||
hierarchy.loadedTiles.forEach((tile) => {
|
||||
if (tile.bbox.overlapsWith(bbox)) {
|
||||
result.push(tile)
|
||||
}
|
||||
})
|
||||
return result;
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,32 @@
|
|||
import TileHierarchy from "./TileHierarchy";
|
||||
import {UIEventSource} from "../../UIEventSource";
|
||||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import TileHierarchy from "./TileHierarchy"
|
||||
import { UIEventSource } from "../../UIEventSource"
|
||||
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer & Tiled> {
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<number, FeatureSourceForLayer & Tiled>();
|
||||
public readonly layer: FilteredLayer;
|
||||
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<number, UIEventSource<FeatureSource[]>>();
|
||||
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void;
|
||||
public readonly loadedTiles: Map<number, FeatureSourceForLayer & Tiled> = new Map<
|
||||
number,
|
||||
FeatureSourceForLayer & Tiled
|
||||
>()
|
||||
public readonly layer: FilteredLayer
|
||||
private readonly sources: Map<number, UIEventSource<FeatureSource[]>> = new Map<
|
||||
number,
|
||||
UIEventSource<FeatureSource[]>
|
||||
>()
|
||||
private _handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource, index: number) => void
|
||||
|
||||
constructor(layer: FilteredLayer, handleTile: (src: FeatureSourceForLayer & IndexedFeatureSource & Tiled, index: number) => void) {
|
||||
this.layer = layer;
|
||||
this._handleTile = handleTile;
|
||||
constructor(
|
||||
layer: FilteredLayer,
|
||||
handleTile: (
|
||||
src: FeatureSourceForLayer & IndexedFeatureSource & Tiled,
|
||||
index: number
|
||||
) => void
|
||||
) {
|
||||
this.layer = layer
|
||||
this._handleTile = handleTile
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,22 +35,24 @@ export class TileHierarchyMerger implements TileHierarchy<FeatureSourceForLayer
|
|||
* @param src
|
||||
*/
|
||||
public registerTile(src: FeatureSource & Tiled) {
|
||||
|
||||
const index = src.tileIndex
|
||||
if (this.sources.has(index)) {
|
||||
const sources = this.sources.get(index)
|
||||
sources.data.push(src)
|
||||
sources.ping()
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// We have to setup
|
||||
const sources = new UIEventSource<FeatureSource[]>([src])
|
||||
this.sources.set(index, sources)
|
||||
const merger = new FeatureSourceMerger(this.layer, index, BBox.fromTile(...Tiles.tile_from_index(index)), sources)
|
||||
const merger = new FeatureSourceMerger(
|
||||
this.layer,
|
||||
index,
|
||||
BBox.fromTile(...Tiles.tile_from_index(index)),
|
||||
sources
|
||||
)
|
||||
this.loadedTiles.set(index, merger)
|
||||
this._handleTile(merger, index)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,53 +1,65 @@
|
|||
import FeatureSource, {FeatureSourceForLayer, IndexedFeatureSource, Tiled} from "../FeatureSource";
|
||||
import {Store, UIEventSource} from "../../UIEventSource";
|
||||
import FilteredLayer from "../../../Models/FilteredLayer";
|
||||
import TileHierarchy from "./TileHierarchy";
|
||||
import {Tiles} from "../../../Models/TileRange";
|
||||
import {BBox} from "../../BBox";
|
||||
import FeatureSource, { FeatureSourceForLayer, IndexedFeatureSource, Tiled } from "../FeatureSource"
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||
import TileHierarchy from "./TileHierarchy"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
|
||||
/**
|
||||
* Contains all features in a tiled fashion.
|
||||
* The data will be automatically broken down into subtiles when there are too much features in a single tile or if the zoomlevel is too high
|
||||
*/
|
||||
export default class TiledFeatureSource implements Tiled, IndexedFeatureSource, FeatureSourceForLayer, TileHierarchy<IndexedFeatureSource & FeatureSourceForLayer & Tiled> {
|
||||
public readonly z: number;
|
||||
public readonly x: number;
|
||||
public readonly y: number;
|
||||
public readonly parent: TiledFeatureSource;
|
||||
export default class TiledFeatureSource
|
||||
implements
|
||||
Tiled,
|
||||
IndexedFeatureSource,
|
||||
FeatureSourceForLayer,
|
||||
TileHierarchy<IndexedFeatureSource & FeatureSourceForLayer & Tiled>
|
||||
{
|
||||
public readonly z: number
|
||||
public readonly x: number
|
||||
public readonly y: number
|
||||
public readonly parent: TiledFeatureSource
|
||||
public readonly root: TiledFeatureSource
|
||||
public readonly layer: FilteredLayer;
|
||||
public readonly layer: FilteredLayer
|
||||
/* An index of all known tiles. allTiles[z][x][y].get('layerid') will yield the corresponding tile.
|
||||
* Only defined on the root element!
|
||||
* Only defined on the root element!
|
||||
*/
|
||||
public readonly loadedTiles: Map<number, TiledFeatureSource & FeatureSourceForLayer> = undefined;
|
||||
public readonly loadedTiles: Map<number, TiledFeatureSource & FeatureSourceForLayer> = undefined
|
||||
|
||||
public readonly maxFeatureCount: number;
|
||||
public readonly name;
|
||||
public readonly features: UIEventSource<{ feature: any, freshness: Date }[]>
|
||||
public readonly maxFeatureCount: number
|
||||
public readonly name
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>
|
||||
public readonly containedIds: Store<Set<string>>
|
||||
|
||||
public readonly bbox: BBox;
|
||||
public readonly tileIndex: number;
|
||||
public readonly bbox: BBox
|
||||
public readonly tileIndex: number
|
||||
private upper_left: TiledFeatureSource
|
||||
private upper_right: TiledFeatureSource
|
||||
private lower_left: TiledFeatureSource
|
||||
private lower_right: TiledFeatureSource
|
||||
private readonly maxzoom: number;
|
||||
private readonly maxzoom: number
|
||||
private readonly options: TiledFeatureSourceOptions
|
||||
|
||||
private constructor(z: number, x: number, y: number, parent: TiledFeatureSource, options?: TiledFeatureSourceOptions) {
|
||||
this.z = z;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
private constructor(
|
||||
z: number,
|
||||
x: number,
|
||||
y: number,
|
||||
parent: TiledFeatureSource,
|
||||
options?: TiledFeatureSourceOptions
|
||||
) {
|
||||
this.z = z
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.bbox = BBox.fromTile(z, x, y)
|
||||
this.tileIndex = Tiles.tile_index(z, x, y)
|
||||
this.name = `TiledFeatureSource(${z},${x},${y})`
|
||||
this.parent = parent;
|
||||
this.parent = parent
|
||||
this.layer = options.layer
|
||||
options = options ?? {}
|
||||
this.maxFeatureCount = options?.maxFeatureCount ?? 250;
|
||||
this.maxFeatureCount = options?.maxFeatureCount ?? 250
|
||||
this.maxzoom = options.maxZoomLevel ?? 18
|
||||
this.options = options;
|
||||
this.options = options
|
||||
if (parent === undefined) {
|
||||
throw "Parent is not allowed to be undefined. Use null instead"
|
||||
}
|
||||
|
@ -55,50 +67,51 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
throw "Invalid root tile: z, x and y should all be null"
|
||||
}
|
||||
if (parent === null) {
|
||||
this.root = this;
|
||||
this.root = this
|
||||
this.loadedTiles = new Map()
|
||||
} else {
|
||||
this.root = this.parent.root;
|
||||
this.loadedTiles = this.root.loadedTiles;
|
||||
this.root = this.parent.root
|
||||
this.loadedTiles = this.root.loadedTiles
|
||||
const i = Tiles.tile_index(z, x, y)
|
||||
this.root.loadedTiles.set(i, this)
|
||||
}
|
||||
this.features = new UIEventSource<any[]>([])
|
||||
this.containedIds = this.features.map(features => {
|
||||
this.containedIds = this.features.map((features) => {
|
||||
if (features === undefined) {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
return new Set(features.map(f => f.feature.properties.id))
|
||||
return new Set(features.map((f) => f.feature.properties.id))
|
||||
})
|
||||
|
||||
// We register this tile, but only when there is some data in it
|
||||
if (this.options.registerTile !== undefined) {
|
||||
this.features.addCallbackAndRunD(features => {
|
||||
this.features.addCallbackAndRunD((features) => {
|
||||
if (features.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.options.registerTile(this)
|
||||
return true;
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static createHierarchy(features: FeatureSource, options?: TiledFeatureSourceOptions): TiledFeatureSource {
|
||||
public static createHierarchy(
|
||||
features: FeatureSource,
|
||||
options?: TiledFeatureSourceOptions
|
||||
): TiledFeatureSource {
|
||||
options = {
|
||||
...options,
|
||||
layer: features["layer"] ?? options.layer
|
||||
layer: features["layer"] ?? options.layer,
|
||||
}
|
||||
const root = new TiledFeatureSource(0, 0, 0, null, options)
|
||||
features.features?.addCallbackAndRunD(feats => root.addFeatures(feats))
|
||||
return root;
|
||||
features.features?.addCallbackAndRunD((feats) => root.addFeatures(feats))
|
||||
return root
|
||||
}
|
||||
|
||||
private isSplitNeeded(featureCount: number) {
|
||||
if (this.upper_left !== undefined) {
|
||||
// This tile has been split previously, so we keep on splitting
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
if (this.z >= this.maxzoom) {
|
||||
// We are not allowed to split any further
|
||||
|
@ -111,7 +124,6 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
|
||||
// To much features - we split
|
||||
return featureCount > this.maxFeatureCount
|
||||
|
||||
}
|
||||
|
||||
/***
|
||||
|
@ -120,21 +132,45 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
* @param features
|
||||
* @private
|
||||
*/
|
||||
private addFeatures(features: { feature: any, freshness: Date }[]) {
|
||||
private addFeatures(features: { feature: any; freshness: Date }[]) {
|
||||
if (features === undefined || features.length === 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.isSplitNeeded(features.length)) {
|
||||
this.features.setData(features)
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (this.upper_left === undefined) {
|
||||
this.upper_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2, this, this.options)
|
||||
this.upper_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2, this, this.options)
|
||||
this.lower_left = new TiledFeatureSource(this.z + 1, this.x * 2, this.y * 2 + 1, this, this.options)
|
||||
this.lower_right = new TiledFeatureSource(this.z + 1, this.x * 2 + 1, this.y * 2 + 1, this, this.options)
|
||||
this.upper_left = new TiledFeatureSource(
|
||||
this.z + 1,
|
||||
this.x * 2,
|
||||
this.y * 2,
|
||||
this,
|
||||
this.options
|
||||
)
|
||||
this.upper_right = new TiledFeatureSource(
|
||||
this.z + 1,
|
||||
this.x * 2 + 1,
|
||||
this.y * 2,
|
||||
this,
|
||||
this.options
|
||||
)
|
||||
this.lower_left = new TiledFeatureSource(
|
||||
this.z + 1,
|
||||
this.x * 2,
|
||||
this.y * 2 + 1,
|
||||
this,
|
||||
this.options
|
||||
)
|
||||
this.lower_right = new TiledFeatureSource(
|
||||
this.z + 1,
|
||||
this.x * 2 + 1,
|
||||
this.y * 2 + 1,
|
||||
this,
|
||||
this.options
|
||||
)
|
||||
}
|
||||
|
||||
const ulf = []
|
||||
|
@ -147,7 +183,7 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
const bbox = BBox.get(feature.feature)
|
||||
|
||||
// There are a few strategies to deal with features that cross tile boundaries
|
||||
|
||||
|
||||
if (this.options.noDuplicates) {
|
||||
// Strategy 1: We put the feature into a somewhat matching tile
|
||||
if (bbox.overlapsWith(this.upper_left.bbox)) {
|
||||
|
@ -195,19 +231,18 @@ export default class TiledFeatureSource implements Tiled, IndexedFeatureSource,
|
|||
this.lower_left.addFeatures(llf)
|
||||
this.lower_right.addFeatures(lrf)
|
||||
this.features.setData(overlapsboundary)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export interface TiledFeatureSourceOptions {
|
||||
readonly maxFeatureCount?: number,
|
||||
readonly maxZoomLevel?: number,
|
||||
readonly minZoomLevel?: number,
|
||||
readonly maxFeatureCount?: number
|
||||
readonly maxZoomLevel?: number
|
||||
readonly minZoomLevel?: number
|
||||
/**
|
||||
* IF minZoomLevel is set, and if a feature runs through a tile boundary, it would normally be duplicated.
|
||||
* Setting 'dontEnforceMinZoomLevel' will assign to feature to some matching subtile.
|
||||
*/
|
||||
readonly noDuplicates?: boolean,
|
||||
readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void,
|
||||
readonly noDuplicates?: boolean
|
||||
readonly registerTile?: (tile: TiledFeatureSource & FeatureSourceForLayer & Tiled) => void
|
||||
readonly layer?: FilteredLayer
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue