Reformat all files with prettier

This commit is contained in:
Pieter Vander Vennet 2022-09-08 21:40:48 +02:00
parent e22d189376
commit b541d3eab4
382 changed files with 50893 additions and 35566 deletions

View file

@ -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 {
}
}
})
}
}
}

View file

@ -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)
}
})
}
}
}

View file

@ -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)
}
}
}

View file

@ -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()
}
}
}

View file

@ -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
}

View file

@ -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())
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
})
}
}

View file

@ -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 })
})
}
}

View file

@ -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()
}
})
}
}
}

View file

@ -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])
})
}
}
}

View file

@ -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
})
}
}
}

View file

@ -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 }[]>([])
}
}
}

View file

@ -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)
}
}

View file

@ -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))
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}
})
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}