refactoring

This commit is contained in:
Pieter Vander Vennet 2023-03-28 05:13:48 +02:00
parent b94a8f5745
commit 5d0fe31c41
114 changed files with 2412 additions and 2958 deletions

View file

@ -2,11 +2,12 @@ import { Changes } from "../Osm/Changes"
import Constants from "../../Models/Constants"
import { UIEventSource } from "../UIEventSource"
import { Utils } from "../../Utils"
import { Feature } from "geojson"
export default class PendingChangesUploader {
private lastChange: Date
constructor(changes: Changes, selectedFeature: UIEventSource<any>) {
constructor(changes: Changes, selectedFeature: UIEventSource<Feature>) {
const self = this
this.lastChange = new Date()
changes.pendingChanges.addCallback(() => {

View file

@ -2,12 +2,19 @@ import { Store, UIEventSource } from "../UIEventSource"
import Locale from "../../UI/i18n/Locale"
import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer"
import Combine from "../../UI/Base/Combine"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ElementStorage } from "../ElementStorage"
import { Utils } from "../../Utils"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Feature } from "geojson"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
export default class TitleHandler {
constructor(selectedElement: Store<any>, layout: LayoutConfig, allElements: ElementStorage) {
constructor(
selectedElement: Store<Feature>,
selectedLayer: Store<LayerConfig>,
allElements: FeaturePropertiesStore,
layout: LayoutConfig
) {
const currentTitle: Store<string> = selectedElement.map(
(selected) => {
const defaultTitle = layout?.title?.txt ?? "MapComplete"
@ -17,13 +24,14 @@ export default class TitleHandler {
}
const tags = selected.properties
for (const layer of layout.layers) {
for (const layer of layout?.layers ?? []) {
if (layer.title === undefined) {
continue
}
if (layer.source.osmTags.matchesProperties(tags)) {
const tagsSource =
allElements.getEventSourceById(tags.id) ?? new UIEventSource<any>(tags)
allElements.getStore(tags.id) ??
new UIEventSource<Record<string, string>>(tags)
const title = new TagRenderingAnswer(tagsSource, layer.title, {})
return (
new Combine([defaultTitle, " | ", title]).ConstructElement()
@ -33,7 +41,7 @@ export default class TitleHandler {
}
return defaultTitle
},
[Locale.language]
[Locale.language, selectedLayer]
)
currentTitle.addCallbackAndRunD((title) => {

View file

@ -1,39 +1,31 @@
/// Given a feature source, calculates a list of OSM-contributors who mapped the latest versions
import { Store, UIEventSource } from "./UIEventSource"
import FeaturePipeline from "./FeatureSource/FeaturePipeline"
import Loc from "../Models/Loc"
import { BBox } from "./BBox"
import GeoIndexedStore from "./FeatureSource/Actors/GeoIndexedStore"
export default class ContributorCount {
public readonly Contributors: UIEventSource<Map<string, number>> = new UIEventSource<
Map<string, number>
>(new Map<string, number>())
private readonly state: {
featurePipeline: FeaturePipeline
currentBounds: Store<BBox>
locationControl: Store<Loc>
}
private readonly perLayer: ReadonlyMap<string, GeoIndexedStore>
private lastUpdate: Date = undefined
constructor(state: {
featurePipeline: FeaturePipeline
currentBounds: Store<BBox>
locationControl: Store<Loc>
bounds: Store<BBox>
dataIsLoading: Store<boolean>
perLayer: ReadonlyMap<string, GeoIndexedStore>
}) {
this.state = state
this.perLayer = state.perLayer
const self = this
state.currentBounds.map((bbox) => {
self.update(bbox)
})
state.featurePipeline.runningQuery.addCallbackAndRun((_) =>
self.update(state.currentBounds.data)
state.bounds.mapD(
(bbox) => {
self.update(bbox)
},
[state.dataIsLoading]
)
}
private update(bbox: BBox) {
if (bbox === undefined) {
return
}
const now = new Date()
if (
this.lastUpdate !== undefined &&
@ -42,7 +34,9 @@ export default class ContributorCount {
return
}
this.lastUpdate = now
const featuresList = this.state.featurePipeline.GetAllFeaturesWithin(bbox)
const featuresList = [].concat(
Array.from(this.perLayer.values()).map((fs) => fs.GetFeaturesWithin(bbox))
)
const hist = new Map<string, number>()
for (const list of featuresList) {
for (const feature of list) {

View file

@ -1,6 +1,5 @@
import { GeoOperations } from "./GeoOperations"
import Combine from "../UI/Base/Combine"
import RelationsTracker from "./Osm/RelationsTracker"
import BaseUIElement from "../UI/BaseUIElement"
import List from "../UI/Base/List"
import Title from "../UI/Base/Title"

View file

@ -0,0 +1,41 @@
import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource"
import { Feature } from "geojson"
import { BBox } from "../../BBox"
import { GeoOperations } from "../../GeoOperations"
import { Store } from "../../UIEventSource"
import FilteredLayer from "../../../Models/FilteredLayer"
/**
* Allows the retrieval of all features in the requested BBox; useful for one-shot queries;
*
* Use a ClippedFeatureSource for a continuously updating featuresource
*/
export default class GeoIndexedStore implements FeatureSource {
public features: Store<Feature[]>
constructor(features: FeatureSource | Store<Feature[]>) {
this.features = features["features"] ?? features
}
/**
* Gets the current features within the given bbox.
*
* @param bbox
* @constructor
*/
public GetFeaturesWithin(bbox: BBox): Feature[] {
// TODO optimize
const bboxFeature = bbox.asGeoJson({})
return this.features.data.filter(
(f) => GeoOperations.intersect(f, bboxFeature) !== undefined
)
}
}
export class GeoIndexedStoreForLayer extends GeoIndexedStore implements FeatureSourceForLayer {
readonly layer: FilteredLayer
constructor(features: FeatureSource | Store<Feature[]>, layer: FilteredLayer) {
super(features)
this.layer = layer
}
}

View file

@ -1,11 +1,10 @@
import { FeatureSourceForLayer, Tiled } from "../FeatureSource"
import MetaTagging from "../../MetaTagging"
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 {

View file

@ -0,0 +1,36 @@
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 { Feature } from "geojson"
import TileLocalStorage from "./TileLocalStorage"
import { GeoOperations } from "../../GeoOperations"
import { Utils } from "../../../Utils"
/***
* Saves all the features that are passed in to localstorage, so they can be retrieved on the next run
*
* The data is saved in a tiled way on a fixed zoomlevel and is retrievable per layer.
*
* Also see the sibling class
*/
export default class SaveFeatureSourceToLocalStorage {
constructor(layername: string, zoomlevel: number, features: FeatureSource) {
const storage = TileLocalStorage.construct<Feature[]>(layername)
features.features.addCallbackAndRunD((features) => {
const sliced = GeoOperations.slice(zoomlevel, features)
sliced.forEach((features, tileIndex) => {
const src = storage.getTileSource(tileIndex)
if (Utils.sameList(src.data, features)) {
return
}
src.setData(features)
})
})
}
}

View file

@ -1,149 +0,0 @@
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 { Feature } from "geojson"
/***
* Saves all the features that are passed in to localstorage, so they can be retrieved on the next run
*
* Technically, more an Actor then a featuresource, but it fits more neatly this way
*/
export default class SaveTileToLocalStorageActor {
private readonly visitedTiles: UIEventSource<Map<number, Date>>
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) => {
for (const key of Array.from(tiles.keys())) {
const tileFreshness = tiles.get(key)
const toOld =
this.initializeTime.getTime() - tileFreshness.getTime() >
1000 * this._layer.maxAgeOfCache
if (toOld) {
// Purge this tile
this.SetIdb(key, undefined)
console.debug("Purging tile", this._layer.id, key)
tiles.delete(key)
}
}
this.visitedTiles.ping()
return true
})
}
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) => {
if (tiles.size === 0) {
// We don't do anything yet as probably not yet loaded from disk
// We'll unregister later on
return
}
currentBounds.addCallbackAndRunD((bbox) => {
if (self._layer.minzoomVisible > location.data.zoom) {
// Not enough zoom
return
}
// Iterate over all available keys in the local storage, check which are needed and fresh enough
for (const key of Array.from(tiles.keys())) {
const tileFreshness = tiles.get(key)
if (tileFreshness > self.initializeTime) {
// This tile is loaded by another source
continue
}
registerFreshness(key, tileFreshness)
const tileBbox = BBox.fromTileIndex(key)
if (!bbox.overlapsWith(tileBbox)) {
continue
}
if (loadedTiles.has(key)) {
// Already loaded earlier
continue
}
loadedTiles.add(key)
this.GetIdb(key).then((features: Feature[]) => {
if (features === undefined) {
return
}
console.debug("Loaded tile " + self._layer.id + "_" + key + " from disk")
const src = new SimpleFeatureSource(
self._flayer,
key,
new UIEventSource<Feature[]>(features)
)
registerTile(src)
})
}
})
return true // Remove the callback
})
}
public addTile(tile: FeatureSource & Tiled) {
const self = this
tile.features.addCallbackAndRunD((features) => {
const now = new Date()
if (features.length > 0) {
self.SetIdb(tile.tileIndex, features)
}
// We _still_ write the time to know that this tile is empty!
this.MarkVisited(tile.tileIndex, now)
})
}
public poison(lon: number, lat: number) {
for (let z = 0; z < 25; 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) {
this.visitedTiles.data.set(tileId, freshness)
this.visitedTiles.ping()
}
private SetIdb(tileIndex, data) {
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
)
}
}
private GetIdb(tileIndex) {
return IdbLocalStorage.GetDirectly(this._layer.id + "_" + tileIndex)
}
}

View file

@ -0,0 +1,63 @@
import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
import { UIEventSource } from "../../UIEventSource"
/**
* A class which allows to read/write a tile to local storage.
*
* Does the heavy lifting for LocalStorageFeatureSource and SaveFeatureToLocalStorage
*/
export default class TileLocalStorage<T> {
private static perLayer: Record<string, TileLocalStorage<any>> = {}
private readonly _layername: string
private readonly cachedSources: Record<number, UIEventSource<T>> = {}
private constructor(layername: string) {
this._layername = layername
}
public static construct<T>(layername: string): TileLocalStorage<T> {
const cached = TileLocalStorage.perLayer[layername]
if (cached) {
return cached
}
const tls = new TileLocalStorage<T>(layername)
TileLocalStorage.perLayer[layername] = tls
return tls
}
/**
* Constructs a UIEventSource element which is synced with localStorage
* @param layername
* @param tileIndex
*/
public getTileSource(tileIndex: number): UIEventSource<T> {
const cached = this.cachedSources[tileIndex]
if (cached) {
return cached
}
const src = UIEventSource.FromPromise(this.GetIdb(tileIndex))
src.addCallbackD((data) => this.SetIdb(tileIndex, data))
this.cachedSources[tileIndex] = src
return src
}
private SetIdb(tileIndex: number, data): void {
try {
IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data)
} catch (e) {
console.error(
"Could not save tile to indexed-db: ",
e,
"tileIndex is:",
tileIndex,
"for layer",
this._layername
)
}
}
private GetIdb(tileIndex: number): Promise<any> {
return IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex)
}
}

View file

@ -1,581 +0,0 @@
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 { NewGeometryFromChangesFeatureSource } from "./Sources/NewGeometryFromChangesFeatureSource"
import ChangeGeometryApplicator from "./Sources/ChangeGeometryApplicator"
/**
* Keeps track of the age of the loaded data.
* Has one freshness-Calculator for every layer
* @private
*/
import { BBox } from "../BBox"
import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource"
import { Tiles } from "../../Models/TileRange"
import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource"
import MapState from "../State/MapState"
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:
*
* - The Overpass-API
* - The OSM-API
* - Third-party geojson files, either sliced or directly.
*
* In order to truly understand this class, please have a look at the following diagram: https://cdn-images-1.medium.com/fit/c/800/618/1*qTK1iCtyJUr4zOyw4IFD7A.jpeg
*
*
*/
export default class FeaturePipeline {
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)
/**
* Keeps track of all raw OSM-nodes.
* Only initialized if `ReplaceGeometryAction` is needed somewhere
*/
public readonly fullNodeDatabase?: FullNodeDatabaseSource
private readonly overpassUpdater: OverpassFeatureSource
private state: MapState
private readonly perLayerHierarchy: Map<string, TileHierarchyMerger>
private readonly oldestAllowedDate: Date
private readonly osmSourceZoomLevel
private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>()
private readonly newGeometryHandler: NewGeometryFromChangesFeatureSource
constructor(
handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
state: MapState,
options?: {
/*Used for metatagging - will receive all the sources with changeGeometry applied but without filtering*/
handleRawFeatureSource: (source: FeatureSourceForLayer) => void
}
) {
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)
)
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
})
const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed)
const perLayerHierarchy = new Map<string, TileHierarchyMerger>()
this.perLayerHierarchy = perLayerHierarchy
// Given a tile, wraps it and passes it on to render (handled by 'handleFeatureSource'
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 srcFiltered = new FilteringFeatureSource(state, src.tileIndex, withChanges)
handleFeatureSource(srcFiltered)
if (options?.handleRawFeatureSource) {
options.handleRawFeatureSource(withChanges)
}
self.somethingLoaded.setData(true)
// We do not mark as visited here, this is the responsability of the code near the actual loader (e.g. overpassLoader and OSMApiFeatureLoader)
}
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)
)
perLayerHierarchy.set(id, hierarchy)
if (id === "type_node") {
this.fullNodeDatabase = new FullNodeDatabaseSource(filteredLayer, (tile) => {
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
})
continue
}
const localTileSaver = new SaveTileToLocalStorageActor(filteredLayer)
this.localStorageSavers.set(filteredLayer.layerDef.id, localTileSaver)
if (source.geojsonSource === undefined) {
// This is an OSM layer
// 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),
(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))
}
)
continue
}
if (source.geojsonZoomLevel === undefined) {
// This is a 'load everything at once' geojson layer
const src = new GeoJsonSource(filteredLayer)
if (source.isOsmCacheLayer) {
// We split them up into tiles anyway as it is an OSM source
TiledFeatureSource.createHierarchy(src, {
layer: src.layer,
minZoomLevel: this.osmSourceZoomLevel,
noDuplicates: true,
registerTile: (tile) => {
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
perLayerHierarchy.get(id).registerTile(tile)
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
},
})
} else {
new RegisteringAllFromFeatureSourceActor(src, state.allElements)
perLayerHierarchy.get(id).registerTile(src)
src.features.addCallbackAndRunD((_) => self.onNewDataLoaded(src))
}
} else {
new DynamicGeoJsonTileSource(
filteredLayer,
(tile) => {
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
perLayerHierarchy.get(id).registerTile(tile)
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
},
state
)
}
}
const osmFeatureSource = new OsmFeatureSource({
isActive: useOsmApi,
neededTiles: neededTilesFromOsm,
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
)
}
saver?.addTile(tile)
}
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
tile.features.addCallbackAndRunD((_) => self.onNewDataLoaded(tile))
},
state: state,
markTileVisited: (tileId) =>
state.filteredLayers.data.forEach((flayer) => {
const layer = flayer.layerDef
if (layer.maxAgeOfCache > 0) {
const saver = self.localStorageSavers.get(layer.id)
if (saver === undefined) {
console.error("No local storage saver found for ", layer.id)
} else {
saver.MarkVisited(tileId, new Date())
}
}
self.freshnesses.get(layer.id).addTileLoad(tileId, new Date())
}),
})
if (this.fullNodeDatabase !== undefined) {
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) =>
this.fullNodeDatabase.handleOsmJson(osmJson, tileId)
)
}
const updater = this.initOverpassUpdater(state, useOsmApi)
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)
})
},
}),
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) => {
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,
(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)
})
},
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]
)
}
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
}
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)),
})
})
return tiles
}
/**
* Gets all the tiles which overlap with the given BBOX.
* This might imply that extra features might be shown
*/
public GetFeaturesWithin(layerId: string, bbox: BBox): OsmFeature[][] {
if (layerId === "*") {
return this.GetAllFeaturesWithin(bbox)
}
const requestedHierarchy = this.perLayerHierarchy.get(layerId)
if (requestedHierarchy === undefined) {
console.warn(
"Layer ",
layerId,
"is not defined. Try one of ",
Array.from(this.perLayerHierarchy.keys())
)
return undefined
}
return TileHierarchyTools.getTiles(requestedHierarchy, bbox)
.filter((featureSource) => featureSource.features?.data !== undefined)
.map((featureSource) => <OsmFeature[]>featureSource.features.data)
}
public GetTilesPerLayerWithin(
bbox: BBox,
handleTile: (tile: FeatureSourceForLayer & Tiled) => void
) {
Array.from(this.perLayerHierarchy.values()).forEach((hierarchy) => {
TileHierarchyTools.getTiles(hierarchy, bbox).forEach(handleTile)
})
}
private onNewDataLoaded(src: FeatureSource) {
this.newDataLoadedSignal.setData(src)
}
private freshnessForVisibleLayers(z: number, x: number, y: number): Date {
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
}
if (flayer.layerDef.maxAgeOfCache === 0) {
return undefined
}
const freshnessCalc = this.freshnesses.get(flayer.layerDef.id)
if (freshnessCalc === undefined) {
console.warn("No freshness tracker found for ", flayer.layerDef.id)
return undefined
}
const freshness = freshnessCalc.freshnessFor(z, x, y)
if (freshness === undefined) {
// SOmething is undefined --> we return undefined as we have to download
return undefined
}
if (oldestDate === undefined || oldestDate > freshness) {
oldestDate = freshness
}
}
return oldestDate
}
/*
* 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
}
tileIndexes.push(i)
})
return tileIndexes
},
[isSufficientlyZoomed]
)
}
private initOverpassUpdater(
state: {
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]
)
return new OverpassFeatureSource(state, {
padToTiles: state.locationControl.map((l) => Math.min(15, l.zoom + 1)),
isActive: useOsmApi.map((b) => !b && overpassIsActive.data, [overpassIsActive]),
})
}
/**
* Builds upon 'GetAllFeaturesAndMetaWithin', but does stricter BBOX-checking and applies the filters
*/
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)
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) {
continue
}
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]
if (!filtered.isDisplayed.data) {
continue
}
if (seenElements.has(element.properties.id)) {
continue
}
seenElements.add(element.properties.id)
if (!bbox.overlapsWith(BBox.get(element))) {
continue
}
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)
)
) {
continue
}
const center = GeoOperations.centerpointCoordinates(element)
elements.push({
element,
center,
layer: layers[elementsWithMetaElement.layer],
})
}
}
return elements
}
/**
* Inject a new point
*/
InjectNewPoint(geojson) {
this.newGeometryHandler.features.data.push(geojson)
this.newGeometryHandler.features.ping()
}
}

View file

@ -1,4 +1,4 @@
import { Store } from "../UIEventSource"
import { Store, UIEventSource } from "../UIEventSource"
import FilteredLayer from "../../Models/FilteredLayer"
import { BBox } from "../BBox"
import { Feature } from "geojson"
@ -6,6 +6,9 @@ import { Feature } from "geojson"
export default interface FeatureSource {
features: Store<Feature[]>
}
export interface WritableFeatureSource extends FeatureSource {
features: UIEventSource<Feature[]>
}
export interface Tiled {
tileIndex: number

View file

@ -1,48 +1,59 @@
import FeatureSource from "./FeatureSource"
import { Store } from "../UIEventSource"
import FeatureSource, { FeatureSourceForLayer } from "./FeatureSource"
import FilteredLayer from "../../Models/FilteredLayer"
import SimpleFeatureSource from "./Sources/SimpleFeatureSource"
import { Feature } from "geojson"
import { Utils } from "../../Utils"
import { UIEventSource } from "../UIEventSource"
/**
* In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled)
* If this is the case, multiple objects with a different _matching_layer_id are generated.
* In any case, this featureSource marks the objects with _matching_layer_id
*/
export default class PerLayerFeatureSourceSplitter {
export default class PerLayerFeatureSourceSplitter<
T extends FeatureSourceForLayer = SimpleFeatureSource
> {
public readonly perLayer: ReadonlyMap<string, T>
constructor(
layers: Store<FilteredLayer[]>,
handleLayerData: (source: FeatureSource, layer: FilteredLayer) => void,
layers: FilteredLayer[],
upstream: FeatureSource,
options?: {
tileIndex?: number
constructStore?: (features: UIEventSource<Feature[]>, layer: FilteredLayer) => T
handleLeftovers?: (featuresWithoutLayer: any[]) => void
}
) {
const knownLayers = new Map<string, SimpleFeatureSource>()
const knownLayers = new Map<string, T>()
this.perLayer = knownLayers
const layerSources = new Map<string, UIEventSource<Feature[]>>()
function update() {
const features = upstream.features?.data
const constructStore =
options?.constructStore ?? ((store, layer) => new SimpleFeatureSource(layer, store))
for (const layer of layers) {
const src = new UIEventSource<Feature[]>([])
layerSources.set(layer.layerDef.id, src)
knownLayers.set(layer.layerDef.id, <T>constructStore(src, layer))
}
upstream.features.addCallbackAndRunD((features) => {
if (features === undefined) {
return
}
if (layers.data === undefined || layers.data.length === 0) {
if (layers === undefined) {
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[]>()
const noLayerFound = []
const noLayerFound: Feature[] = []
for (const layer of layers.data) {
for (const layer of layers) {
featuresPerLayer.set(layer.layerDef.id, [])
}
for (const f of features) {
let foundALayer = false
for (const layer of layers.data) {
for (const layer of layers) {
if (layer.layerDef.source.osmTags.matchesProperties(f.properties)) {
// We have found our matching layer!
featuresPerLayer.get(layer.layerDef.id).push(f)
@ -60,7 +71,7 @@ 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) {
for (const layer of layers) {
const id = layer.layerDef.id
const features = featuresPerLayer.get(id)
if (features === undefined) {
@ -68,25 +79,24 @@ export default class PerLayerFeatureSourceSplitter {
continue
}
let featureSource = knownLayers.get(id)
if (featureSource === undefined) {
// Not yet initialized - now is a good time
featureSource = new SimpleFeatureSource(layer)
featureSource.features.setData(features)
knownLayers.set(id, featureSource)
handleLayerData(featureSource, layer)
} else {
featureSource.features.setData(features)
const src = layerSources.get(id)
if (Utils.sameList(src.data, features)) {
return
}
src.setData(features)
}
// AT last, the leftovers are handled
if (options?.handleLeftovers !== undefined && noLayerFound.length > 0) {
options.handleLeftovers(noLayerFound)
}
}
})
}
layers.addCallback((_) => update())
upstream.features.addCallbackAndRunD((_) => update())
public forEach(f: (featureSource: FeatureSourceForLayer) => void) {
for (const fs of this.perLayer.values()) {
f(fs)
}
}
}

View file

@ -0,0 +1,17 @@
import FeatureSource from "../FeatureSource"
import { Feature, Polygon } from "geojson"
import StaticFeatureSource from "./StaticFeatureSource"
import { GeoOperations } from "../../GeoOperations"
/**
* Returns a clipped version of the original geojson. Ways which partially intersect the given feature will be split up
*/
export default class ClippedFeatureSource extends StaticFeatureSource {
constructor(features: FeatureSource, clipTo: Feature<Polygon>) {
super(
features.features.mapD((features) => {
return [].concat(features.map((feature) => GeoOperations.clipWith(feature, clipTo)))
})
)
}
}

View file

@ -1,15 +1,15 @@
import { Store, UIEventSource } from "../../UIEventSource"
import FilteredLayer, { FilterState } from "../../../Models/FilteredLayer"
import FilteredLayer from "../../../Models/FilteredLayer"
import FeatureSource from "../FeatureSource"
import { TagsFilter } from "../../Tags/TagsFilter"
import { Feature } from "geojson"
import { OsmTags } from "../../../Models/OsmFeature"
import { GlobalFilter } from "../../../Models/GlobalFilter"
export default class FilteringFeatureSource implements FeatureSource {
public features: UIEventSource<Feature[]> = new UIEventSource([])
private readonly upstream: FeatureSource
private readonly _fetchStore?: (id: String) => Store<OsmTags>
private readonly _globalFilters?: Store<{ filter: FilterState }[]>
private readonly _fetchStore?: (id: string) => Store<Record<string, string>>
private readonly _globalFilters?: Store<GlobalFilter[]>
private readonly _alreadyRegistered = new Set<Store<any>>()
private readonly _is_dirty = new UIEventSource(false)
private readonly _layer: FilteredLayer
@ -18,8 +18,8 @@ export default class FilteringFeatureSource implements FeatureSource {
constructor(
layer: FilteredLayer,
upstream: FeatureSource,
fetchStore?: (id: String) => Store<OsmTags>,
globalFilters?: Store<{ filter: FilterState }[]>,
fetchStore?: (id: string) => Store<Record<string, string>>,
globalFilters?: Store<GlobalFilter[]>,
metataggingUpdated?: Store<any>
) {
this.upstream = upstream
@ -32,9 +32,11 @@ export default class FilteringFeatureSource implements FeatureSource {
self.update()
})
layer.appliedFilters.addCallback((_) => {
self.update()
})
layer.appliedFilters.forEach((value) =>
value.addCallback((_) => {
self.update()
})
)
this._is_dirty.stabilized(1000).addCallbackAndRunD((dirty) => {
if (dirty) {
@ -58,7 +60,7 @@ export default class FilteringFeatureSource implements FeatureSource {
const layer = this._layer
const features: Feature[] = this.upstream.features.data ?? []
const includedFeatureIds = new Set<string>()
const globalFilters = self._globalFilters?.data?.map((f) => f.filter)
const globalFilters = self._globalFilters?.data?.map((f) => f)
const newFeatures = (features ?? []).filter((f) => {
self.registerCallback(f)
@ -71,19 +73,26 @@ export default class FilteringFeatureSource implements FeatureSource {
return false
}
const tagsFilter = Array.from(layer.appliedFilters?.data?.values() ?? [])
for (const filter of tagsFilter) {
const neededTags: TagsFilter = filter?.currentFilter
for (const filter of layer.layerDef.filters) {
const state = layer.appliedFilters.get(filter.id).data
if (state === undefined) {
continue
}
let neededTags: TagsFilter
if (typeof state === "string") {
// This filter uses fields
} else {
neededTags = filter.options[state].osmTags
}
if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) {
// Hidden by the filter on the layer itself - we want to hide it no matter what
return false
}
}
for (const filter of globalFilters ?? []) {
const neededTags: TagsFilter = filter?.currentFilter
for (const globalFilter of globalFilters ?? []) {
const neededTags = globalFilter.osmTags
if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) {
// Hidden by the filter on the layer itself - we want to hide it no matter what
return false
}
}

View file

@ -58,7 +58,7 @@ export default class GeoJsonSource implements FeatureSource {
.replace("{x_max}", "" + bounds.maxLon)
}
const eventsource = new UIEventSource<Feature[]>(undefined)
const eventsource = new UIEventSource<Feature[]>([])
if (options?.isActive !== undefined) {
options.isActive.addCallbackAndRunD(async (active) => {
if (!active) {

View file

@ -1,14 +1,15 @@
import FeatureSource from "./FeatureSource"
import { Store } from "../UIEventSource"
import FeatureSwitchState from "../State/FeatureSwitchState"
import OverpassFeatureSource from "../Actors/OverpassFeatureSource"
import { BBox } from "../BBox"
import OsmFeatureSource from "./TiledFeatureSource/OsmFeatureSource"
import { Or } from "../Tags/Or"
import FeatureSourceMerger from "./Sources/FeatureSourceMerger"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import GeoJsonSource from "./Sources/GeoJsonSource"
import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSource"
import GeoJsonSource from "./GeoJsonSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import FeatureSource from "../FeatureSource"
import { Or } from "../../Tags/Or"
import FeatureSwitchState from "../../State/FeatureSwitchState"
import OverpassFeatureSource from "./OverpassFeatureSource"
import { Store } from "../../UIEventSource"
import OsmFeatureSource from "./OsmFeatureSource"
import FeatureSourceMerger from "./FeatureSourceMerger"
import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource"
import { BBox } from "../../BBox"
import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource"
/**
* This source will fetch the needed data from various sources for the given layout.
@ -17,22 +18,24 @@ import DynamicGeoJsonTileSource from "./TiledFeatureSource/DynamicGeoJsonTileSou
*/
export default class LayoutSource extends FeatureSourceMerger {
constructor(
filteredLayers: LayerConfig[],
layers: LayerConfig[],
featureSwitches: FeatureSwitchState,
newAndChangedElements: FeatureSource,
mapProperties: { bounds: Store<BBox>; zoom: Store<number> },
backend: string,
isLayerActive: (id: string) => Store<boolean>
isDisplayed: (id: string) => Store<boolean>
) {
const { bounds, zoom } = mapProperties
// remove all 'special' layers
filteredLayers = filteredLayers.filter((flayer) => flayer.source !== null)
layers = layers.filter((flayer) => flayer.source !== null)
const geojsonlayers = filteredLayers.filter(
(flayer) => flayer.source.geojsonSource !== undefined
)
const osmLayers = filteredLayers.filter(
(flayer) => flayer.source.geojsonSource === undefined
const geojsonlayers = layers.filter((layer) => layer.source.geojsonSource !== undefined)
const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined)
const fromCache = osmLayers.map(
(l) =>
new LocalStorageFeatureSource(l.id, 15, mapProperties, {
isActive: isDisplayed(l.id),
})
)
const overpassSource = LayoutSource.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
const osmApiSource = LayoutSource.setupOsmApiSource(
@ -43,11 +46,11 @@ export default class LayoutSource extends FeatureSourceMerger {
featureSwitches
)
const geojsonSources: FeatureSource[] = geojsonlayers.map((l) =>
LayoutSource.setupGeojsonSource(l, mapProperties)
LayoutSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
)
const expiryInSeconds = Math.min(...(filteredLayers?.map((l) => l.maxAgeOfCache) ?? []))
super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources)
const expiryInSeconds = Math.min(...(layers?.map((l) => l.maxAgeOfCache) ?? []))
super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources, ...fromCache)
}
private static setupGeojsonSource(
@ -56,6 +59,10 @@ export default class LayoutSource extends FeatureSourceMerger {
isActive?: Store<boolean>
): FeatureSource {
const source = layer.source
isActive = mapProperties.zoom.map(
(z) => (isActive?.data ?? true) && z >= layer.maxzoom,
[isActive]
)
if (source.geojsonZoomLevel === undefined) {
// This is a 'load everything at once' geojson layer
return new GeoJsonSource(layer, { isActive })

View file

@ -108,6 +108,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
}
private async LoadTile(z, x, y): Promise<void> {
console.log("OsmFeatureSource: loading ", z, x, y)
if (z >= 22) {
throw "This is an absurd high zoom level"
}
@ -126,7 +127,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
let error = undefined
try {
const osmJson = await Utils.downloadJson(url)
const osmJson = await Utils.downloadJsonCached(url, 2000)
try {
this.rawDataHandlers.forEach((handler) =>
handler(osmJson, Tiles.tile_index(z, x, y))

View file

@ -1,13 +1,13 @@
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
import { Or } from "../Tags/Or"
import { Overpass } from "../Osm/Overpass"
import FeatureSource from "../FeatureSource/FeatureSource"
import { Utils } from "../../Utils"
import { TagsFilter } from "../Tags/TagsFilter"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { BBox } from "../BBox"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Feature } from "geojson"
import FeatureSource from "../FeatureSource"
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { Or } from "../../Tags/Or"
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
import { Overpass } from "../../Osm/Overpass"
import { Utils } from "../../../Utils"
import { TagsFilter } from "../../Tags/TagsFilter"
import { BBox } from "../../BBox"
/**
* A wrapper around the 'Overpass'-object.
@ -99,7 +99,11 @@ export default class OverpassFeatureSource implements FeatureSource {
) {
return undefined
}
const [bounds, date, updatedLayers] = await this.updateAsync()
const result = await this.updateAsync()
if (!result) {
return
}
const [bounds, date, updatedLayers] = result
this._lastQueryBBox = bounds
}
@ -188,6 +192,9 @@ export default class OverpassFeatureSource implements FeatureSource {
if (data === undefined) {
return undefined
}
// Some metatags are delivered by overpass _without_ underscore-prefix; we fix them below
// TODO FIXME re-enable this data.features.forEach((f) => SimpleMetaTaggers.objectMetaInfo.applyMetaTagsOnFeature(f))
self.features.setData(data.features)
return [bounds, date, layersToDownload]
} catch (e) {

View file

@ -1,34 +0,0 @@
/**
* 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 { Feature } from "geojson"
export default class RememberingSource implements FeatureSource, Tiled {
public readonly features: Store<Feature[]>
public readonly tileIndex: number
public readonly bbox: BBox
constructor(source: FeatureSource & Tiled) {
const self = this
this.tileIndex = source.tileIndex
this.bbox = source.bbox
const empty = []
const featureSource = new UIEventSource<Feature[]>(empty)
this.features = featureSource
source.features.addCallbackAndRunD((features) => {
const oldFeatures = self.features?.data ?? empty
// Then new ids
const ids = new Set<string>(features.map((f) => f.properties.id + f.geometry.type))
// the old data
const oldData = oldFeatures.filter(
(old) => !ids.has(old.feature.properties.id + old.feature.geometry.type)
)
featureSource.setData([...features, ...oldData])
})
}
}

View file

@ -0,0 +1,29 @@
import FeatureSource, { FeatureSourceForLayer } from "../FeatureSource"
import StaticFeatureSource from "./StaticFeatureSource"
import { GeoOperations } from "../../GeoOperations"
import { BBox } from "../../BBox"
import exp from "constants"
import FilteredLayer from "../../../Models/FilteredLayer"
/**
* Results in a feature source which has all the elements that touch the given features
*/
export default class BBoxFeatureSource extends StaticFeatureSource {
constructor(features: FeatureSource, mustTouch: BBox) {
const bbox = mustTouch.asGeoJson({})
super(
features.features.mapD((features) =>
features.filter((feature) => GeoOperations.intersect(feature, bbox) !== undefined)
)
)
}
}
export class BBoxFeatureSourceForLayer extends BBoxFeatureSource implements FeatureSourceForLayer {
constructor(features: FeatureSourceForLayer, mustTouch: BBox) {
super(features, mustTouch)
this.layer = features.layer
}
readonly layer: FilteredLayer
}

View file

@ -84,7 +84,9 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
})
},
mapProperties,
{ isActive: options.isActive }
{
isActive: options?.isActive,
}
)
}
}

View file

@ -5,7 +5,8 @@ import FeatureSource from "../FeatureSource"
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
/***
* A tiled source which dynamically loads the required tiles at a fixed zoom level
* A tiled source which dynamically loads the required tiles at a fixed zoom level.
* A single featureSource will be initiliased for every tile in view; which will alter be merged into this featureSource
*/
export default class DynamicTileSource extends FeatureSourceMerger {
constructor(

View file

@ -1,11 +1,13 @@
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 { OsmTags } from "../../../Models/OsmFeature";
import { BBox } from "../../BBox";
import { Feature, Point } from "geojson";
export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> {
export default class FullNodeDatabaseSource {
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void
private readonly layer: FilteredLayer
@ -81,4 +83,9 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
public GetParentWays(nodeId: number): UIEventSource<OsmWay[]> {
return this.parentWays.get(nodeId)
}
getNodesWithin(bBox: BBox) : Feature<Point, OsmTags>[]{
// TODO
throw "TODO"
}
}

View file

@ -0,0 +1,28 @@
import DynamicTileSource from "./DynamicTileSource"
import { Store } from "../../UIEventSource"
import { BBox } from "../../BBox"
import TileLocalStorage from "../Actors/TileLocalStorage"
import { Feature } from "geojson"
import StaticFeatureSource from "../Sources/StaticFeatureSource"
export default class LocalStorageFeatureSource extends DynamicTileSource {
constructor(
layername: string,
zoomlevel: number,
mapProperties: {
bounds: Store<BBox>
zoom: Store<number>
},
options?: {
isActive?: Store<boolean>
}
) {
const storage = TileLocalStorage.construct<Feature[]>(layername)
super(
zoomlevel,
(tileIndex) => new StaticFeatureSource(storage.getTileSource(tileIndex)),
mapProperties,
options
)
}
}

View file

@ -1,24 +0,0 @@
Data in MapComplete can come from multiple sources.
Currently, they are:
- The Overpass-API
- The OSM-API
- One or more GeoJSON files. This can be a single file or a set of tiled geojson files
- LocalStorage, containing features from a previous visit
- Changes made by the user introducing new features
When the data enters from Overpass or from the OSM-API, they are first distributed per layer:
OVERPASS | ---PerLayerFeatureSource---> FeatureSourceForLayer[]
OSM |
The GeoJSon files (not tiled) are then added to this list
A single FeatureSourcePerLayer is then further handled by splitting it into a tile hierarchy.
In order to keep thins snappy, they are distributed over a tiled database per layer.
## Notes
`cached-featuresbookcases` is the old key used `cahced-features{themeid}` and should be cleaned up

View file

@ -1,24 +0,0 @@
import FeatureSource, { Tiled } from "../FeatureSource"
import { BBox } from "../../BBox"
export default interface TileHierarchy<T extends FeatureSource> {
/**
* 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[] {
const result: T[] = []
hierarchy.loadedTiles.forEach((tile) => {
if (tile.bbox.overlapsWith(bbox)) {
result.push(tile)
}
})
return result
}
}

View file

@ -1,58 +0,0 @@
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
constructor(
layer: FilteredLayer,
handleTile: (
src: FeatureSourceForLayer & IndexedFeatureSource & Tiled,
index: number
) => void
) {
this.layer = layer
this._handleTile = handleTile
}
/**
* Add another feature source for the given tile.
* Entries for this tile will be merged
* @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
}
// 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
)
this.loadedTiles.set(index, merger)
this._handleTile(merger, index)
}
}

View file

@ -1,249 +0,0 @@
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 { Feature } from "geojson";
/**
* 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
public readonly root: TiledFeatureSource
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!
*/
public readonly loadedTiles: Map<number, TiledFeatureSource & FeatureSourceForLayer> = undefined
public readonly maxFeatureCount: number
public readonly name
public readonly features: UIEventSource<Feature[]>
public readonly containedIds: Store<Set<string>>
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 options: TiledFeatureSourceOptions
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.layer = options.layer
options = options ?? {}
this.maxFeatureCount = options?.maxFeatureCount ?? 250
this.maxzoom = options.maxZoomLevel ?? 18
this.options = options
if (parent === undefined) {
throw "Parent is not allowed to be undefined. Use null instead"
}
if (parent === null && z !== 0 && x !== 0 && y !== 0) {
throw "Invalid root tile: z, x and y should all be null"
}
if (parent === null) {
this.root = this
this.loadedTiles = new Map()
} else {
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) => {
if (features === undefined) {
return undefined
}
return new Set(features.map((f) => f.properties.id))
})
// We register this tile, but only when there is some data in it
if (this.options.registerTile !== undefined) {
this.features.addCallbackAndRunD((features) => {
if (features.length === 0) {
return
}
this.options.registerTile(this)
return true
})
}
}
public static createHierarchy(
features: FeatureSource,
options?: TiledFeatureSourceOptions
): TiledFeatureSource {
options = {
...options,
layer: features["layer"] ?? options.layer,
}
const root = new TiledFeatureSource(0, 0, 0, null, options)
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
}
if (this.z >= this.maxzoom) {
// We are not allowed to split any further
return false
}
if (this.options.minZoomLevel !== undefined && this.z < this.options.minZoomLevel) {
// We must have at least this zoom level before we are allowed to start splitting
return true
}
// To much features - we split
return featureCount > this.maxFeatureCount
}
/***
* Adds the list of features to this hierarchy.
* If there are too much features, the list will be broken down and distributed over the subtiles (only retaining features that don't fit a subtile on this level)
* @param features
* @private
*/
private addFeatures(features: Feature[]) {
if (features === undefined || features.length === 0) {
return
}
if (!this.isSplitNeeded(features.length)) {
this.features.setData(features)
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
)
}
const ulf = []
const urf = []
const llf = []
const lrf = []
const overlapsboundary = []
for (const feature of features) {
const bbox = BBox.get(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)) {
ulf.push(feature)
} else if (bbox.overlapsWith(this.upper_right.bbox)) {
urf.push(feature)
} else if (bbox.overlapsWith(this.lower_left.bbox)) {
llf.push(feature)
} else if (bbox.overlapsWith(this.lower_right.bbox)) {
lrf.push(feature)
} else {
overlapsboundary.push(feature)
}
} else if (this.options.minZoomLevel === undefined) {
// Strategy 2: put it into a strictly matching tile (or in this tile, which is slightly too big)
if (bbox.isContainedIn(this.upper_left.bbox)) {
ulf.push(feature)
} else if (bbox.isContainedIn(this.upper_right.bbox)) {
urf.push(feature)
} else if (bbox.isContainedIn(this.lower_left.bbox)) {
llf.push(feature)
} else if (bbox.isContainedIn(this.lower_right.bbox)) {
lrf.push(feature)
} else {
overlapsboundary.push(feature)
}
} else {
// Strategy 3: We duplicate a feature on a boundary into every tile as we need to get to the minZoomLevel
if (bbox.overlapsWith(this.upper_left.bbox)) {
ulf.push(feature)
}
if (bbox.overlapsWith(this.upper_right.bbox)) {
urf.push(feature)
}
if (bbox.overlapsWith(this.lower_left.bbox)) {
llf.push(feature)
}
if (bbox.overlapsWith(this.lower_right.bbox)) {
lrf.push(feature)
}
}
}
this.upper_left.addFeatures(ulf)
this.upper_right.addFeatures(urf)
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
/**
* 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 layer?: FilteredLayer
}

View file

@ -2,19 +2,34 @@ import { BBox } from "./BBox"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import * as turf from "@turf/turf"
import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf"
import { Feature, Geometry, MultiPolygon, Polygon } from "geojson"
import { GeoJSON, LineString, Point, Position } from "geojson"
import {
Feature,
GeoJSON,
Geometry,
LineString,
MultiPolygon,
Point,
Polygon,
Position,
} from "geojson"
import togpx from "togpx"
import Constants from "../Models/Constants"
import { Tiles } from "../Models/TileRange"
export class GeoOperations {
private static readonly _earthRadius = 6378137
private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2
/**
* Create a union between two features
*/
static union = turf.union
static intersect = turf.intersect
private static readonly _earthRadius = 6378137
private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2
public static union(f0: Feature, f1: Feature): Feature<Polygon | MultiPolygon> | null {
return turf.union(<any>f0, <any>f1)
}
public static intersect(f0: Feature, f1: Feature): Feature<Polygon | MultiPolygon> | null {
return turf.intersect(<any>f0, <any>f1)
}
static surfaceAreaInSqMeters(feature: any) {
return turf.area(feature)
@ -637,14 +652,14 @@ export class GeoOperations {
*/
static completelyWithin(
feature: Feature<Geometry, any>,
possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any>
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any>
): boolean {
return booleanWithin(feature, possiblyEncloingFeature)
return booleanWithin(feature, possiblyEnclosingFeature)
}
/**
* Create an intersection between two features.
* A new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary
* One or multiple new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary
*/
public static clipWith(toSplit: Feature, boundary: Feature<Polygon>): Feature[] {
if (toSplit.geometry.type === "Point") {
@ -677,35 +692,6 @@ export class GeoOperations {
throw "Invalid geometry type with GeoOperations.clipWith: " + toSplit.geometry.type
}
/**
* Helper function which does the heavy lifting for 'inside'
*/
private static pointInPolygonCoordinates(
x: number,
y: number,
coordinates: [number, number][][]
): boolean {
const inside = GeoOperations.pointWithinRing(
x,
y,
/*This is the outer ring of the polygon */ coordinates[0]
)
if (!inside) {
return false
}
for (let i = 1; i < coordinates.length; i++) {
const inHole = GeoOperations.pointWithinRing(
x,
y,
coordinates[i] /* These are inner rings, aka holes*/
)
if (inHole) {
return false
}
}
return true
}
/**
*
*
@ -763,6 +749,62 @@ export class GeoOperations {
throw "Unkown location type: " + location
}
}
/**
* Constructs all tiles where features overlap with and puts those features in them.
* Long features (e.g. lines or polygons) which overlap with multiple tiles are referenced in each tile they overlap with
* @param zoomlevel
* @param features
*/
public static slice(zoomlevel: number, features: Feature[]): Map<number, Feature[]> {
const tiles = new Map<number, Feature[]>()
for (const feature of features) {
const bbox = BBox.get(feature)
Tiles.MapRange(Tiles.tileRangeFrom(bbox, zoomlevel), (x, y) => {
const i = Tiles.tile_index(zoomlevel, x, y)
let tiledata = tiles.get(i)
if (tiledata === undefined) {
tiledata = []
tiles.set(i, tiledata)
}
tiledata.push(feature)
})
}
return tiles
}
/**
* Helper function which does the heavy lifting for 'inside'
*/
private static pointInPolygonCoordinates(
x: number,
y: number,
coordinates: [number, number][][]
): boolean {
const inside = GeoOperations.pointWithinRing(
x,
y,
/*This is the outer ring of the polygon */ coordinates[0]
)
if (!inside) {
return false
}
for (let i = 1; i < coordinates.length; i++) {
const inHole = GeoOperations.pointWithinRing(
x,
y,
coordinates[i] /* These are inner rings, aka holes*/
)
if (inHole) {
return false
}
}
return true
}
private static pointWithinRing(x: number, y: number, ring: [number, number][]) {
let inside = false
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {

View file

@ -2,12 +2,12 @@ import { OsmCreateAction } from "./OsmChangeAction"
import { Tag } from "../../Tags/Tag"
import { Changes } from "../Changes"
import { ChangeDescription } from "./ChangeDescription"
import FeaturePipelineState from "../../State/FeaturePipelineState"
import FeatureSource from "../../FeatureSource/FeatureSource"
import CreateNewWayAction from "./CreateNewWayAction"
import CreateWayWithPointReuseAction, { MergePointConfig } from "./CreateWayWithPointReuseAction"
import { And } from "../../Tags/And"
import { TagUtils } from "../../Tags/TagUtils"
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
import FeatureSource from "../../FeatureSource/FeatureSource"
/**
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
@ -26,14 +26,14 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
tags: Tag[],
outerRingCoordinates: [number, number][],
innerRingsCoordinates: [number, number][][],
state: FeaturePipelineState,
state: SpecialVisualizationState,
config: MergePointConfig[],
changeType: "import" | "create" | string
) {
super(null, true)
this._tags = [...tags, new Tag("type", "multipolygon")]
this.changeType = changeType
this.theme = state?.layoutToUse?.id ?? ""
this.theme = state?.layout?.id ?? ""
this.createOuterWay = new CreateWayWithPointReuseAction(
[],
outerRingCoordinates,
@ -45,7 +45,7 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
new CreateNewWayAction(
[],
ringCoordinates.map(([lon, lat]) => ({ lat, lon })),
{ theme: state?.layoutToUse?.id }
{ theme: state?.layout?.id }
)
)
@ -59,6 +59,10 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
}
}
public async getPreview(): Promise<FeatureSource> {
return undefined
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
console.log("Running CMPWPRA")
const descriptions: ChangeDescription[] = []

View file

@ -2,7 +2,6 @@ import { OsmCreateAction } from "./OsmChangeAction"
import { Tag } from "../../Tags/Tag"
import { Changes } from "../Changes"
import { ChangeDescription } from "./ChangeDescription"
import FeaturePipelineState from "../../State/FeaturePipelineState"
import { BBox } from "../../BBox"
import { TagsFilter } from "../../Tags/TagsFilter"
import { GeoOperations } from "../../GeoOperations"
@ -10,6 +9,7 @@ import FeatureSource from "../../FeatureSource/FeatureSource"
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
import CreateNewNodeAction from "./CreateNewNodeAction"
import CreateNewWayAction from "./CreateNewWayAction"
import { SpecialVisualizationState } from "../../../UI/SpecialVisualization"
export interface MergePointConfig {
withinRangeOfM: number
@ -62,14 +62,14 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
* lngLat-coordinates
* @private
*/
private _coordinateInfo: CoordinateInfo[]
private _state: FeaturePipelineState
private _config: MergePointConfig[]
private readonly _coordinateInfo: CoordinateInfo[]
private readonly _state: SpecialVisualizationState
private readonly _config: MergePointConfig[]
constructor(
tags: Tag[],
coordinates: [number, number][],
state: FeaturePipelineState,
state: SpecialVisualizationState,
config: MergePointConfig[]
) {
super(null, true)
@ -188,7 +188,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
}
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
const theme = this._state?.layoutToUse?.id
const theme = this._state?.layout?.id
const allChanges: ChangeDescription[] = []
const nodeIdsToUse: { lat: number; lon: number; nodeId?: number }[] = []
for (let i = 0; i < this._coordinateInfo.length; i++) {
@ -252,9 +252,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] {
const bbox = new BBox(coordinates)
const state = this._state
const allNodes = [].concat(
...(state?.featurePipeline?.GetFeaturesWithin("type_node", bbox.pad(1.2)) ?? [])
)
const allNodes =state.fullNodeDatabase?.getNodesWithin(bbox.pad(1.2))
const maxDistance = Math.max(...this._config.map((c) => c.withinRangeOfM))
// Init coordianteinfo with undefined but the same length as coordinates

View file

@ -12,8 +12,8 @@ import { And } from "../../Tags/And"
import { Utils } from "../../../Utils"
import { OsmConnection } from "../OsmConnection"
import { Feature } from "@turf/turf"
import FeaturePipeline from "../../FeatureSource/FeaturePipeline"
import { Geometry, LineString, Point, Polygon } from "geojson"
import { Geometry, LineString, Point } from "geojson"
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
export default class ReplaceGeometryAction extends OsmChangeAction {
/**
@ -22,7 +22,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
private readonly feature: any
private readonly state: {
osmConnection: OsmConnection
featurePipeline: FeaturePipeline
fullNodeDatabase?: FullNodeDatabaseSource
}
private readonly wayToReplaceId: string
private readonly theme: string
@ -41,7 +41,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
constructor(
state: {
osmConnection: OsmConnection
featurePipeline: FeaturePipeline
fullNodeDatabase?: FullNodeDatabaseSource
},
feature: any,
wayToReplaceId: string,
@ -195,7 +195,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
}> {
// TODO FIXME: if a new point has to be created, snap to already existing ways
const nodeDb = this.state.featurePipeline.fullNodeDatabase
const nodeDb = this.state.fullNodeDatabase
if (nodeDb === undefined) {
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
}
@ -415,7 +415,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
const nodeDb = this.state.featurePipeline.fullNodeDatabase
const nodeDb = this.state.fullNodeDatabase
if (nodeDb === undefined) {
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
}

View file

@ -5,6 +5,10 @@ export interface GeoCodeResult {
display_name: string
lat: number
lon: number
/**
* Format:
* [lat, lat, lon, lon]
*/
boundingbox: number[]
osm_type: "node" | "way" | "relation"
osm_id: string

View file

@ -15,6 +15,13 @@ import { OsmTags } from "../Models/OsmFeature"
import { UIEventSource } from "./UIEventSource"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
/**
* All elements that are needed to perform metatagging
*/
export interface MetataggingState {
layout: LayoutConfig
}
export abstract class SimpleMetaTagger {
public readonly keys: string[]
public readonly doc: string
@ -60,7 +67,7 @@ export abstract class SimpleMetaTagger {
feature: any,
layer: LayerConfig,
tagsStore: UIEventSource<Record<string, string>>,
state: { layout: LayoutConfig }
state: MetataggingState
): boolean
}
@ -119,7 +126,7 @@ export class CountryTagger extends SimpleMetaTagger {
})
}
applyMetaTagsOnFeature(feature, _, state) {
applyMetaTagsOnFeature(feature, _, tagsSource) {
let centerPoint: any = GeoOperations.centerpoint(feature)
const runningTasks = this.runningTasks
const lat = centerPoint.geometry.coordinates[1]
@ -128,28 +135,29 @@ export class CountryTagger extends SimpleMetaTagger {
CountryTagger.coder
.GetCountryCodeAsync(lon, lat)
.then((countries) => {
runningTasks.delete(feature)
try {
const oldCountry = feature.properties["_country"]
feature.properties["_country"] = countries[0].trim().toLowerCase()
if (oldCountry !== feature.properties["_country"]) {
const tagsSource = state?.allElements?.getEventSourceById(
feature.properties.id
)
tagsSource?.ping()
}
} catch (e) {
console.warn(e)
const oldCountry = feature.properties["_country"]
const newCountry = countries[0].trim().toLowerCase()
if (oldCountry !== newCountry) {
tagsSource.data["_country"] = newCountry
tagsSource?.ping()
}
})
.catch((_) => {
runningTasks.delete(feature)
.catch((e) => {
console.warn(e)
})
.finally(() => runningTasks.delete(feature))
return false
}
}
class InlineMetaTagger extends SimpleMetaTagger {
public readonly applyMetaTagsOnFeature: (
feature: any,
layer: LayerConfig,
tagsStore: UIEventSource<OsmTags>,
state: MetataggingState
) => boolean
constructor(
docs: {
keys: string[]
@ -166,23 +174,17 @@ class InlineMetaTagger extends SimpleMetaTagger {
feature: any,
layer: LayerConfig,
tagsStore: UIEventSource<OsmTags>,
state: { layout: LayoutConfig }
state: MetataggingState
) => boolean
) {
super(docs)
this.applyMetaTagsOnFeature = f
}
public readonly applyMetaTagsOnFeature: (
feature: any,
layer: LayerConfig,
tagsStore: UIEventSource<OsmTags>,
state: { layout: LayoutConfig }
) => boolean
}
export default class SimpleMetaTaggers {
public static readonly objectMetaInfo = new InlineMetaTagger(
{
export class RewriteMetaInfoTags extends SimpleMetaTagger {
constructor() {
super({
keys: [
"_last_edit:contributor",
"_last_edit:contributor:uid",
@ -192,30 +194,37 @@ export default class SimpleMetaTaggers {
"_backend",
],
doc: "Information about the last edit of this object.",
},
(feature) => {
/*Note: also called by 'UpdateTagsFromOsmAPI'*/
})
}
const tgs = feature.properties
let movedSomething = false
applyMetaTagsOnFeature(feature: Feature): boolean {
/*Note: also called by 'UpdateTagsFromOsmAPI'*/
function move(src: string, target: string) {
if (tgs[src] === undefined) {
return
}
tgs[target] = tgs[src]
delete tgs[src]
movedSomething = true
const tgs = feature.properties
let movedSomething = false
function move(src: string, target: string) {
if (tgs[src] === undefined) {
return
}
move("user", "_last_edit:contributor")
move("uid", "_last_edit:contributor:uid")
move("changeset", "_last_edit:changeset")
move("timestamp", "_last_edit:timestamp")
move("version", "_version_number")
return movedSomething
tgs[target] = tgs[src]
delete tgs[src]
movedSomething = true
}
)
move("user", "_last_edit:contributor")
move("uid", "_last_edit:contributor:uid")
move("changeset", "_last_edit:changeset")
move("timestamp", "_last_edit:timestamp")
move("version", "_version_number")
return movedSomething
}
}
export default class SimpleMetaTaggers {
/**
* A simple metatagger which rewrites various metatags as needed
*/
public static readonly objectMetaInfo = new RewriteMetaInfoTags()
public static country = new CountryTagger()
public static geometryType = new InlineMetaTagger(
{

View file

@ -1,10 +1,5 @@
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import FeaturePipeline from "../FeatureSource/FeaturePipeline"
import { Tiles } from "../../Models/TileRange"
import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"
import Hash from "../Web/Hash"
import { BBox } from "../BBox"
import { FeatureSourceForLayer, Tiled } from "../FeatureSource/FeatureSource"
import MetaTagRecalculator from "../FeatureSource/Actors/MetaTagRecalculator"
export default class FeaturePipelineState {
@ -14,101 +9,9 @@ export default class FeaturePipelineState {
public readonly featurePipeline: FeaturePipeline
private readonly metatagRecalculator: MetaTagRecalculator
constructor(layoutToUse: LayoutConfig) {
const clustering = layoutToUse?.clustering
const clusterCounter = this.featureAggregator
const self = this
/**
* We are a bit in a bind:
* There is the featurePipeline, which creates some sources during construction
* THere is the metatagger, which needs to have these sources registered AND which takes a FeaturePipeline as argument
*
* This is a bit of a catch-22 (except that it isn't)
* The sources that are registered in the constructor are saved into 'registeredSources' temporary
*
*/
const sourcesToRegister = []
function registerRaw(source: FeatureSourceForLayer & Tiled) {
if (self.metatagRecalculator === undefined) {
sourcesToRegister.push(source)
} else {
self.metatagRecalculator.registerSource(source)
}
}
function registerSource(source: FeatureSourceForLayer & Tiled) {
clusterCounter.addTile(source)
const sourceBBox = source.features.map((allFeatures) =>
BBox.bboxAroundAll(allFeatures.map(BBox.get))
)
// Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering
source.features.map(
(f) => {
const z = self.locationControl.data.zoom
if (!source.layer.isDisplayed.data) {
return false
}
const bounds = self.currentBounds.data
if (bounds === undefined) {
// Map is not yet displayed
return false
}
if (!sourceBBox.data.overlapsWith(bounds)) {
// Not within range -> features are hidden
return false
}
if (z < source.layer.layerDef.minzoom) {
// Layer is always hidden for this zoom level
return false
}
if (z > clustering.maxZoom) {
return true
}
if (f.length > clustering.minNeededElements) {
// This tile alone already has too much features
return false
}
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex)
if (tileZ >= z) {
while (tileZ > z) {
tileZ--
tileX = Math.floor(tileX / 2)
tileY = Math.floor(tileY / 2)
}
if (
clusterCounter.getTile(Tiles.tile_index(tileZ, tileX, tileY))
?.totalValue > clustering.minNeededElements
) {
// To much elements
return false
}
}
return true
},
[self.currentBounds, source.layer.isDisplayed, sourceBBox]
)
}
this.featurePipeline = new FeaturePipeline(registerSource, this, {
handleRawFeatureSource: registerRaw,
})
constructor() {
this.metatagRecalculator = new MetaTagRecalculator(this, this.featurePipeline)
this.metatagRecalculator.registerSource(this.currentView)
sourcesToRegister.forEach((source) => self.metatagRecalculator.registerSource(source))
new SelectedFeatureHandler(Hash.hash, this)
}
}

View file

@ -1,10 +1,8 @@
import { UIEventSource } from "../UIEventSource"
import { GlobalFilter } from "../../Models/GlobalFilter"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import FilteredLayer from "../../Models/FilteredLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { OsmConnection } from "../Osm/OsmConnection"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { QueryParameters } from "../Web/QueryParameters"
/**
* The layer state keeps track of:
@ -36,83 +34,14 @@ export default class LayerState {
this.osmConnection = osmConnection
this.filteredLayers = new Map()
for (const layer of layers) {
this.filteredLayers.set(layer.id, this.initFilteredLayer(layer, context))
this.filteredLayers.set(
layer.id,
FilteredLayer.initLinkedState(layer, context, this.osmConnection)
)
}
layers.forEach((l) => this.linkFilterStates(l))
}
private static getPref(
osmConnection: OsmConnection,
key: string,
layer: LayerConfig
): UIEventSource<boolean> {
return osmConnection.GetPreference(key, layer.shownByDefault + "").sync(
(v) => {
if (v === undefined) {
return undefined
}
return v === "true"
},
[],
(b) => {
if (b === undefined) {
return undefined
}
return "" + b
}
)
}
/**
* INitializes a filtered layer for the given layer.
* @param layer
* @param context: probably the theme-name. This is used to disambiguate the user settings; e.g. when using the same layer in different contexts
* @private
*/
private initFilteredLayer(layer: LayerConfig, context: string): FilteredLayer | undefined {
let isDisplayed: UIEventSource<boolean>
const osmConnection = this.osmConnection
if (layer.syncSelection === "local") {
isDisplayed = LocalStorageSource.GetParsed(
context + "-layer-" + layer.id + "-enabled",
layer.shownByDefault
)
} else if (layer.syncSelection === "theme-only") {
isDisplayed = LayerState.getPref(
osmConnection,
context + "-layer-" + layer.id + "-enabled",
layer
)
} else if (layer.syncSelection === "global") {
isDisplayed = LayerState.getPref(osmConnection, "layer-" + layer.id + "-enabled", layer)
} else {
isDisplayed = QueryParameters.GetBooleanQueryParameter(
"layer-" + layer.id,
layer.shownByDefault,
"Wether or not layer " + layer.id + " is shown"
)
}
const flayer: FilteredLayer = {
isDisplayed,
layerDef: layer,
appliedFilters: new UIEventSource<Map<string, FilterState>>(
new Map<string, FilterState>()
),
}
layer.filters?.forEach((filterConfig) => {
const stateSrc = filterConfig.initState()
stateSrc.addCallbackAndRun((state) =>
flayer.appliedFilters.data.set(filterConfig.id, state)
)
flayer.appliedFilters
.map((dict) => dict.get(filterConfig.id))
.addCallback((state) => stateSrc.setData(state))
})
return flayer
}
/**
* Some layers copy the filter state of another layer - this is quite often the case for 'sibling'-layers,
* (where two variations of the same layer are used, e.g. a specific type of shop on all zoom levels and all shops on high zoom).
@ -136,10 +65,6 @@ export default class LayerState {
console.warn(
"Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs
)
this.filteredLayers.set(layer.id, {
isDisplayed: toReuse.isDisplayed,
layerDef: layer,
appliedFilters: toReuse.appliedFilters,
})
this.filteredLayers.set(layer.id, toReuse)
}
}

View file

@ -17,14 +17,10 @@ export default class UserRelatedState {
The user credentials
*/
public osmConnection: OsmConnection
/**
THe change handler
*/
public changes: Changes
/**
* The key for mangrove
*/
public mangroveIdentity: MangroveIdentity
public readonly mangroveIdentity: MangroveIdentity
public readonly installedUserThemes: Store<string[]>

View file

@ -63,27 +63,10 @@ export class Stores {
stable.setData(undefined)
return
}
const oldList = stable.data
if (oldList === list) {
if (Utils.sameList(stable.data, list)) {
return
}
if (oldList == list) {
return
}
if (oldList === undefined || oldList.length !== list.length) {
stable.setData(list)
return
}
for (let i = 0; i < list.length; i++) {
if (oldList[i] !== list[i]) {
stable.setData(list)
return
}
}
// No actual changes, so we don't do anything
return
stable.setData(list)
})
return stable
}
@ -93,7 +76,7 @@ export abstract class Store<T> implements Readable<T> {
abstract readonly data: T
/**
* OPtional value giving a title to the UIEventSource, mainly used for debugging
* Optional value giving a title to the UIEventSource, mainly used for debugging
*/
public readonly tag: string | undefined
@ -794,4 +777,14 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
update(f: Updater<T> & ((value: T) => T)): void {
this.setData(f(this.data))
}
/**
* Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well.
* However, this value can be overriden without affecting source
*/
static feedFrom<T>(store: Store<T>): UIEventSource<T> {
const src = new UIEventSource(store.data)
store.addCallback((t) => src.setData(t))
return src
}
}

View file

@ -1,10 +1,8 @@
import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
import { MangroveReviews, Review } from "mangrove-reviews-typescript"
import { Utils } from "../../Utils"
import { Feature, Geometry, Position } from "geojson"
import { Feature, Position } from "geojson"
import { GeoOperations } from "../GeoOperations"
import { OsmTags } from "../../Models/OsmFeature"
import { ElementStorage } from "../ElementStorage"
export class MangroveIdentity {
public readonly keypair: Store<CryptoKeyPair>
@ -67,11 +65,9 @@ export default class FeatureReviews {
private readonly _identity: MangroveIdentity
private constructor(
feature: Feature<Geometry, OsmTags>,
state: {
allElements: ElementStorage
mangroveIdentity?: MangroveIdentity
},
feature: Feature,
tagsSource: UIEventSource<Record<string, string>>,
mangroveIdentity?: MangroveIdentity,
options?: {
nameKey?: "name" | string
fallbackName?: string
@ -80,8 +76,7 @@ export default class FeatureReviews {
) {
const centerLonLat = GeoOperations.centerpointCoordinates(feature)
;[this._lon, this._lat] = centerLonLat
this._identity =
state?.mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined))
this._identity = mangroveIdentity ?? new MangroveIdentity(new UIEventSource<string>(undefined))
const nameKey = options?.nameKey ?? "name"
if (feature.geometry.type === "Point") {
@ -108,9 +103,7 @@ export default class FeatureReviews {
this._uncertainty = options?.uncertaintyRadius ?? maxDistance
}
this._name = state.allElements
.getEventSourceById(feature.properties.id)
.map((tags) => tags[nameKey] ?? options?.fallbackName)
this._name = tagsSource .map((tags) => tags[nameKey] ?? options?.fallbackName)
this.subjectUri = this.ConstructSubjectUri()
@ -136,11 +129,9 @@ export default class FeatureReviews {
* Construct a featureReviewsFor or fetches it from the cache
*/
public static construct(
feature: Feature<Geometry, OsmTags>,
state: {
allElements: ElementStorage
mangroveIdentity?: MangroveIdentity
},
feature: Feature,
tagsSource: UIEventSource<Record<string, string>>,
mangroveIdentity?: MangroveIdentity,
options?: {
nameKey?: "name" | string
fallbackName?: string
@ -152,7 +143,7 @@ export default class FeatureReviews {
if (cached !== undefined) {
return cached
}
const featureReviews = new FeatureReviews(feature, state, options)
const featureReviews = new FeatureReviews(feature, tagsSource, mangroveIdentity, options)
FeatureReviews._featureReviewsCache[key] = featureReviews
return featureReviews
}