Docs: improve typing of FeatureSources

This commit is contained in:
Pieter Vander Vennet 2025-08-20 01:12:09 +02:00
parent c71036d6c6
commit 34924fd4b1
21 changed files with 140 additions and 127 deletions

View file

@ -36,6 +36,6 @@ export interface FeatureSourceForTile<T extends Feature = Feature> extends Featu
/**
* A feature source which is aware of the indexes it contains
*/
export interface IndexedFeatureSource extends FeatureSource {
export interface IndexedFeatureSource<T extends Feature> extends FeatureSource<T> {
readonly featuresById: Store<Map<string, Feature>>
}

View file

@ -1,17 +1,16 @@
import { Store, UIEventSource } from "../../UIEventSource"
import { FeatureSource, IndexedFeatureSource, UpdatableFeatureSource } from "../FeatureSource"
import { Feature } from "geojson"
import { OsmFeature } from "../../../Models/OsmFeature"
import { Lists } from "../../../Utils/Lists"
/**
* The featureSourceMerger receives complete geometries from various sources.
* If multiple sources contain the same object (as determined by 'id'), only one copy of them is retained
*/
export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSource>
implements IndexedFeatureSource
export default class FeatureSourceMerger<T extends Feature, Src extends FeatureSource<T> = FeatureSource<T>>
implements IndexedFeatureSource<T>
{
public features: UIEventSource<Feature[]> = new UIEventSource([])
public features: UIEventSource<T[]> = new UIEventSource([])
public readonly featuresById: Store<Map<string, Feature>>
protected readonly _featuresById: UIEventSource<Map<string, Feature>>
protected readonly _sources: Src[]
@ -55,7 +54,7 @@ export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSour
* Returns 'true' if this was a previously unseen item.
* If the item was already present, nothing will happen
*/
public addItem(f: OsmFeature): boolean {
public addItem(f: T): boolean {
const id = f.properties.id
const all = this._featuresById.data
@ -68,10 +67,10 @@ export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSour
}
}
protected addData(sources: Feature[][]) {
protected addData(sources: T[][]) {
sources = Lists.noNull(sources)
let somethingChanged = false
const all: Map<string, Feature> = new Map()
const all: Map<string, T> = new Map()
const unseen = new Set<string>()
// We seed the dictionary with the previously loaded features
const oldValues = this.features.data ?? []
@ -118,10 +117,11 @@ export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSour
}
export class UpdatableFeatureSourceMerger<
Src extends UpdatableFeatureSource = UpdatableFeatureSource
T extends Feature,
Src extends UpdatableFeatureSource<T> = UpdatableFeatureSource<T>
>
extends FeatureSourceMerger<Src>
implements IndexedFeatureSource, UpdatableFeatureSource
extends FeatureSourceMerger<T, Src>
implements IndexedFeatureSource<T>, UpdatableFeatureSource<T>
{
constructor(...sources: Src[]) {
super(...sources)

View file

@ -3,13 +3,15 @@ import { Utils } from "../../../Utils"
import { FeatureSource } from "../FeatureSource"
import { BBox } from "../../BBox"
import { GeoOperations } from "../../GeoOperations"
import { Feature } from "geojson"
import { Feature, Geometry } from "geojson"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { Tiles } from "../../../Models/TileRange"
export default class GeoJsonSource implements FeatureSource {
private readonly _features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>(undefined)
public readonly features: Store<Feature[]> = this._features
export default class GeoJsonSource<T extends Feature<Geometry, {
id: string
} & Record<string, any>>> implements FeatureSource<T> {
private readonly _features: UIEventSource<T[]> = new UIEventSource(undefined)
public readonly features: Store<T[]> = this._features
private readonly seenids: Set<string>
private readonly idKey?: string
private readonly url: string
@ -96,7 +98,7 @@ export default class GeoJsonSource implements FeatureSource {
const url = this.url
try {
const cacheAge = (options?.maxCacheAgeSec ?? 300) * 1000
let json = <{ features: Feature[] }>await Utils.downloadJsonCached(url, cacheAge)
let json = <{ features: T[] }>await Utils.downloadJsonCached(url, cacheAge)
if (json.features === undefined || json.features === null) {
json.features = []
@ -106,7 +108,7 @@ export default class GeoJsonSource implements FeatureSource {
json = GeoOperations.GeoJsonToWGS84(json)
}
const newFeatures: Feature[] = []
const newFeatures: T[] = []
let i = 0
for (const feature of json.features) {
if (feature.geometry.type === "Point") {

View file

@ -5,8 +5,8 @@ import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource"
import { MvtToGeojson } from "mvt-to-geojson"
import { OsmTags } from "../../../Models/OsmFeature"
export default class MvtSource implements FeatureSourceForTile, UpdatableFeatureSource {
public readonly features: Store<GeojsonFeature<Geometry, OsmTags>[]>
export default class MvtSource<T extends Feature<Geometry, OsmTags>> implements FeatureSourceForTile<T>, UpdatableFeatureSource<T> {
public readonly features: Store<T[]>
public readonly x: number
public readonly y: number
public readonly z: number

View file

@ -4,16 +4,16 @@ import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
import { Tiles } from "../../../Models/TileRange"
import { BBox } from "../../BBox"
import { TagsFilter } from "../../Tags/TagsFilter"
import { Feature } from "geojson"
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"
import { Lists } from "../../../Utils/Lists"
import { OsmFeature } from "../../../Models/OsmFeature"
/**
* If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile'
*/
export default class OsmFeatureSource extends FeatureSourceMerger {
export default class OsmFeatureSource<T extends OsmFeature> extends FeatureSourceMerger<T> {
private readonly _bounds: Store<BBox>
private readonly isActive: Store<boolean>
private readonly _backend: string
@ -33,7 +33,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false)
private readonly _downloadedTiles: Set<number> = new Set<number>()
private readonly _downloadedData: Feature[][] = []
private readonly _downloadedData: T[][] = []
private readonly _patchRelations: boolean
/**
* Downloads data directly from the OSM-api within the given bounds.
@ -90,7 +90,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
}
}
private registerFeatures(features: Feature[]): void {
private registerFeatures(features: T[]): void {
this._downloadedData.push(features)
super.addData(this._downloadedData)
}
@ -160,7 +160,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
const osmJson = await Utils.downloadJsonCached(url, 2000)
try {
this.options?.fullNodeDatabase?.handleOsmJson(osmJson, z, x, y)
let features = <Feature<any, { id: string }>[]>OsmToGeoJson(osmJson, {
let features = <T[]>OsmToGeoJson(osmJson, {
flatProperties: true,
}).features

View file

@ -1,4 +1,3 @@
import { Feature, FeatureCollection, Geometry } from "geojson"
import { UpdatableFeatureSource } from "../FeatureSource"
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
@ -7,20 +6,20 @@ import { Overpass } from "../../Osm/Overpass"
import { Utils } from "../../../Utils"
import { TagsFilter } from "../../Tags/TagsFilter"
import { BBox } from "../../BBox"
import { OsmTags } from "../../../Models/OsmFeature"
import { OsmFeature } from "../../../Models/OsmFeature"
import { Lists } from "../../../Utils/Lists"
;("use strict")
("use strict")
/**
* A wrapper around the 'Overpass'-object.
* It has more logic and will automatically fetch the data for the right bbox and the active layers
*/
export default class OverpassFeatureSource implements UpdatableFeatureSource {
export default class OverpassFeatureSource<T extends OsmFeature = OsmFeature> implements UpdatableFeatureSource<T> {
/**
* The last loaded features, as geojson
*/
public readonly features: UIEventSource<Feature[]> = new UIEventSource(undefined)
public readonly features: UIEventSource<T[]> = new UIEventSource(undefined)
public readonly runningQuery: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public readonly timeout: UIEventSource<number> = new UIEventSource<number>(0)
@ -111,7 +110,7 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
if (!navigator.onLine) {
return
}
let data: FeatureCollection<Geometry, OsmTags> = undefined
let data: { features: T[] } = undefined
let lastUsed = 0
const start = new Date()
const layersToDownload = this._layersToDownload.data
@ -143,7 +142,7 @@ export default class OverpassFeatureSource implements UpdatableFeatureSource {
return undefined
}
this.runningQuery.setData(true)
data = (await overpass.queryGeoJson(bounds))[0]
data = (await overpass.queryGeoJson<T>(bounds))[0]
} catch (e) {
this.retries.data++
this.retries.ping()

View file

@ -26,9 +26,6 @@ export default class StaticFeatureSource<T extends Feature = Feature> implements
}
}
public static fromGeojson<T extends Feature>(geojson: T[]): StaticFeatureSource<T> {
return new StaticFeatureSource(geojson)
}
}
export class WritableStaticFeatureSource<T extends Feature = Feature>

View file

@ -12,7 +12,7 @@ import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeature
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource"
import DynamicMvtileSource from "../TiledFeatureSource/DynamicMvtTileSource"
import FeatureSourceMerger from "./FeatureSourceMerger"
import { Feature } from "geojson"
import { Feature, Geometry } from "geojson"
import { OsmFeature } from "../../../Models/OsmFeature"
/**
@ -20,7 +20,7 @@ import { OsmFeature } from "../../../Models/OsmFeature"
*
* Note that special layers (with `source=null` will be ignored)
*/
export default class ThemeSource implements IndexedFeatureSource {
export default class ThemeSource<T extends Feature<Geometry, Record<string, any> & {id: string}>> implements IndexedFeatureSource<T> {
/**
* Indicates if a data source is loading something
*/
@ -28,12 +28,12 @@ export default class ThemeSource implements IndexedFeatureSource {
public static readonly fromCacheZoomLevel = 15
public features: UIEventSource<Feature[]> = new UIEventSource([])
public readonly featuresById: Store<Map<string, Feature>>
private readonly core: Store<ThemeSourceCore>
public features: UIEventSource<T[]> = new UIEventSource([])
public readonly featuresById: Store<Map<string, T>>
private readonly core: Store<ThemeSourceCore<T>>
private readonly addedSources: FeatureSource[] = []
private readonly addedItems: OsmFeature[] = []
private readonly addedSources: FeatureSource<T>[] = []
private readonly addedItems: T[] = []
constructor(
layers: LayerConfig[],
@ -47,11 +47,11 @@ export default class ThemeSource implements IndexedFeatureSource {
const isLoading = new UIEventSource(true)
this.isLoading = isLoading
const features = (this.features = new UIEventSource<Feature[]>([]))
const features = (this.features = new UIEventSource<T[]>([]))
const featuresById = (this.featuresById = new UIEventSource(new Map()))
this.core = mvtAvailableLayers.mapD((mvtAvailableLayers) => {
this.core?.data?.destruct()
const core = new ThemeSourceCore(
const core = new ThemeSourceCore<T>(
layers,
featureSwitches,
mapProperties,
@ -73,12 +73,12 @@ export default class ThemeSource implements IndexedFeatureSource {
return this.core.data.downloadAll()
}
public addSource(source: FeatureSource) {
public addSource(source: FeatureSource<T>) {
this.core.data?.addSource(source)
this.addedSources.push(source)
}
public addItem(obj: OsmFeature) {
public addItem(obj: T) {
this.core.data?.addItem(obj)
this.addedItems.push(obj)
}
@ -89,7 +89,7 @@ export default class ThemeSource implements IndexedFeatureSource {
*
* Note that special layers (with `source=null` will be ignored)
*/
class ThemeSourceCore extends FeatureSourceMerger {
class ThemeSourceCore<T extends OsmFeature> extends FeatureSourceMerger<T> {
/**
* This source is _only_ triggered when the data is downloaded for CSV export
* @private
@ -113,10 +113,10 @@ class ThemeSourceCore extends FeatureSourceMerger {
const geojsonlayers = layers.filter((layer) => layer.source.geojsonSource !== undefined)
const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined)
const fromCache = new Map<string, LocalStorageFeatureSource>()
const fromCache = new Map<string, LocalStorageFeatureSource<T>>()
if (featureSwitches.featureSwitchCache.data) {
for (const layer of osmLayers) {
const src = new LocalStorageFeatureSource(
const src = new LocalStorageFeatureSource<T>(
backend,
layer,
ThemeSource.fromCacheZoomLevel,
@ -129,13 +129,13 @@ class ThemeSourceCore extends FeatureSourceMerger {
fromCache.set(layer.id, src)
}
}
const mvtSources: UpdatableFeatureSource[] = osmLayers
const mvtSources: UpdatableFeatureSource<T>[] = osmLayers
.filter((f) => mvtAvailableLayers.has(f.id))
.map((l) => ThemeSourceCore.setupMvtSource(l, mapProperties, isDisplayed(l.id)))
const nonMvtSources: FeatureSource[] = []
.map((l) => ThemeSourceCore.setupMvtSource<T>(l, mapProperties, isDisplayed(l.id)))
const nonMvtSources: FeatureSource<T>[] = []
const nonMvtLayers: LayerConfig[] = osmLayers.filter((l) => !mvtAvailableLayers.has(l.id))
const osmApiSource = ThemeSourceCore.setupOsmApiSource(
const osmApiSource = ThemeSourceCore.setupOsmApiSource<T>(
osmLayers,
bounds,
zoom,
@ -145,14 +145,14 @@ class ThemeSourceCore extends FeatureSourceMerger {
)
nonMvtSources.push(osmApiSource)
let overpassSource: OverpassFeatureSource = undefined
let overpassSource: OverpassFeatureSource<T> = undefined
if (nonMvtLayers.length > 0) {
console.log(
"Layers ",
nonMvtLayers.map((l) => l.id),
" cannot be fetched from the cache server, defaulting to overpass/OSM-api"
)
overpassSource = ThemeSourceCore.setupOverpass(osmLayers, bounds, zoom, featureSwitches)
overpassSource = ThemeSourceCore.setupOverpass<T>(osmLayers, bounds, zoom, featureSwitches)
nonMvtSources.push(overpassSource)
}
@ -164,11 +164,11 @@ class ThemeSourceCore extends FeatureSourceMerger {
overpassSource?.runningQuery?.addCallbackAndRun(() => setIsLoading())
osmApiSource?.isRunning?.addCallbackAndRun(() => setIsLoading())
const geojsonSources: UpdatableFeatureSource[] = geojsonlayers.map((l) =>
const geojsonSources: UpdatableFeatureSource<T>[] = geojsonlayers.map((l) =>
ThemeSourceCore.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
)
const downloadAll = new OverpassFeatureSource(
const downloadAll = new OverpassFeatureSource<T>(
{
layers: layers.filter((l) => l.isNormal()),
bounds: mapProperties.bounds,
@ -196,19 +196,19 @@ class ThemeSourceCore extends FeatureSourceMerger {
this._mapBounds = mapProperties.bounds
}
private static setupMvtSource(
private static setupMvtSource<T extends OsmFeature>(
layer: LayerConfig,
mapProperties: { zoom: Store<number>; bounds: Store<BBox> },
isActive?: Store<boolean>
): UpdatableFeatureSource {
return new DynamicMvtileSource(layer, mapProperties, { isActive })
): UpdatableFeatureSource<T> {
return new DynamicMvtileSource<T>(layer, mapProperties, { isActive })
}
private static setupGeojsonSource(
private static setupGeojsonSource<T extends OsmFeature>(
layer: LayerConfig,
mapProperties: { zoom: Store<number>; bounds: Store<BBox> },
isActiveByFilter?: Store<boolean>
): UpdatableFeatureSource {
): UpdatableFeatureSource<T> {
const source = layer.source
const isActive = mapProperties.zoom.map(
(z) => (isActiveByFilter?.data ?? true) && z >= layer.minzoom,
@ -216,20 +216,20 @@ class ThemeSourceCore extends FeatureSourceMerger {
)
if (source.geojsonZoomLevel === undefined) {
// This is a 'load everything at once' geojson layer
return new GeoJsonSource(layer, { isActive })
return new GeoJsonSource<T>(layer, { isActive })
} else {
return new DynamicGeoJsonTileSource(layer, mapProperties, { isActive })
return new DynamicGeoJsonTileSource<T>(layer, mapProperties, { isActive })
}
}
private static setupOsmApiSource(
private static setupOsmApiSource<T extends OsmFeature>(
osmLayers: LayerConfig[],
bounds: Store<BBox>,
zoom: Store<number>,
backend: string,
featureSwitches: FeatureSwitchState,
fullNodeDatabase: FullNodeDatabaseSource
): OsmFeatureSource | undefined {
): OsmFeatureSource<T> | undefined {
if (osmLayers.length == 0) {
return undefined
}
@ -248,7 +248,7 @@ class ThemeSourceCore extends FeatureSourceMerger {
if (typeof allowedFeatures === "boolean") {
throw "Invalid filter to init OsmFeatureSource: it optimizes away to " + allowedFeatures
}
return new OsmFeatureSource({
return new OsmFeatureSource<T>({
allowedFeatures,
bounds,
backend,
@ -258,12 +258,12 @@ class ThemeSourceCore extends FeatureSourceMerger {
})
}
private static setupOverpass(
private static setupOverpass<T extends OsmFeature>(
osmLayers: LayerConfig[],
bounds: Store<BBox>,
zoom: Store<number>,
featureSwitches: FeatureSwitchState
): OverpassFeatureSource | undefined {
): OverpassFeatureSource<T> | undefined {
if (osmLayers.length == 0) {
return undefined
}

View file

@ -4,8 +4,9 @@ import { Utils } from "../../../Utils"
import GeoJsonSource from "../Sources/GeoJsonSource"
import { BBox } from "../../BBox"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { Feature, Geometry } from "geojson"
export default class DynamicGeoJsonTileSource extends UpdatableDynamicTileSource {
export default class DynamicGeoJsonTileSource<T extends Feature<Geometry, Record<string, any> & {id: string} > > extends UpdatableDynamicTileSource<T> {
private static whitelistCache = new Map<string, Map<number, Set<number>>>()
constructor(

View file

@ -9,8 +9,10 @@ import Constants from "../../../Models/Constants"
import { UpdatableFeatureSourceMerger } from "../Sources/FeatureSourceMerger"
import { LineSourceMerger } from "./LineSourceMerger"
import { PolygonSourceMerger } from "./PolygonSourceMerger"
import { OsmFeature, OsmTags } from "../../../Models/OsmFeature"
import { Feature, Point } from "geojson"
class PolygonMvtSource extends PolygonSourceMerger {
class PolygonMvtSource<P extends Record<string, any> & { id: string }> extends PolygonSourceMerger<P> {
constructor(
layer: LayerConfig,
mapProperties: {
@ -44,7 +46,7 @@ class PolygonMvtSource extends PolygonSourceMerger {
}
}
class LineMvtSource extends LineSourceMerger {
class LineMvtSource extends LineSourceMerger<OsmTags> {
constructor(
layer: LayerConfig,
mapProperties: {
@ -78,7 +80,7 @@ class LineMvtSource extends LineSourceMerger {
}
}
class PointMvtSource extends UpdatableDynamicTileSource {
class PointMvtSource<T extends Feature<Point, OsmTags>> extends UpdatableDynamicTileSource<T> {
constructor(
layer: LayerConfig,
mapProperties: {
@ -102,7 +104,7 @@ class PointMvtSource extends UpdatableDynamicTileSource {
layer: layer.id,
type: "pois",
})
return new MvtSource(url, x, y, z)
return new MvtSource<T>(url, x, y, z)
},
mapProperties,
{
@ -112,7 +114,7 @@ class PointMvtSource extends UpdatableDynamicTileSource {
}
}
export default class DynamicMvtileSource extends UpdatableFeatureSourceMerger {
export default class DynamicMvtileSource<T extends OsmFeature> extends UpdatableFeatureSourceMerger<T> {
constructor(
layer: LayerConfig,
mapProperties: {
@ -124,9 +126,9 @@ export default class DynamicMvtileSource extends UpdatableFeatureSourceMerger {
}
) {
super(
new PointMvtSource(layer, mapProperties, options),
new LineMvtSource(layer, mapProperties, options),
new PolygonMvtSource(layer, mapProperties, options)
<any>new PointMvtSource(layer, mapProperties, options),
<any>new LineMvtSource(layer, mapProperties, options),
<any>new PolygonMvtSource(layer, mapProperties, options),
)
}
}

View file

@ -3,14 +3,15 @@ import { Tiles } from "../../../Models/TileRange"
import { BBox } from "../../BBox"
import { FeatureSource, UpdatableFeatureSource } from "../FeatureSource"
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
import { Feature, Geometry } from "geojson"
/***
* A tiled source which dynamically loads the required tiles at a fixed zoom level.
* A single featureSource will be initialized for every tile in view; which will later be merged into this featureSource
*/
export default class DynamicTileSource<
Src extends FeatureSource = FeatureSource
> extends FeatureSourceMerger<Src> {
export default class DynamicTileSource<T extends Feature,
Src extends FeatureSource<T> = FeatureSource<T>
> extends FeatureSourceMerger<T, Src> {
private readonly loadedTiles = new Set<number>()
private readonly zDiff: number
private readonly zoomlevel: Store<number>
@ -97,9 +98,9 @@ export default class DynamicTileSource<
}
}
export class UpdatableDynamicTileSource<Src extends UpdatableFeatureSource = UpdatableFeatureSource>
extends DynamicTileSource<Src>
implements UpdatableFeatureSource
export class UpdatableDynamicTileSource<T extends Feature<Geometry, Record<string, any> & {id: string}>, Src extends UpdatableFeatureSource<T> = UpdatableFeatureSource<T>>
extends DynamicTileSource<T, Src>
implements UpdatableFeatureSource<T>
{
constructor(
zoomlevel: Store<number>,

View file

@ -1,8 +1,7 @@
import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource"
import { Store } from "../../UIEventSource"
import { BBox } from "../../BBox"
import { Utils } from "../../../Utils"
import { Feature, MultiLineString, Position } from "geojson"
import { Feature, LineString, MultiLineString, Position } from "geojson"
import { GeoOperations } from "../../GeoOperations"
import { UpdatableDynamicTileSource } from "./DynamicTileSource"
import { Lists } from "../../../Utils/Lists"
@ -11,15 +10,16 @@ import { Lists } from "../../../Utils/Lists"
* The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together.
* This is used to reconstruct polygons of vector tiles
*/
export class LineSourceMerger extends UpdatableDynamicTileSource<
FeatureSourceForTile & UpdatableFeatureSource
export class LineSourceMerger<P extends Record<string, any> & { id: string }> extends UpdatableDynamicTileSource<
Feature<LineString | MultiLineString, P>, FeatureSourceForTile<Feature<LineString | MultiLineString, P>> & UpdatableFeatureSource<Feature<LineString | MultiLineString, P>>
> {
private readonly _zoomlevel: Store<number>
constructor(
zoomlevel: Store<number>,
minzoom: number,
constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource,
constructSource: (tileIndex: number) => FeatureSourceForTile<
Feature<LineString | MultiLineString, P>> & UpdatableFeatureSource<Feature<LineString | MultiLineString, P>>,
mapProperties: {
bounds: Store<BBox>
zoom: Store<number>
@ -32,9 +32,9 @@ export class LineSourceMerger extends UpdatableDynamicTileSource<
this._zoomlevel = zoomlevel
}
protected addDataFromSources(sources: FeatureSourceForTile[]) {
protected addDataFromSources(sources: FeatureSourceForTile<Feature<LineString | MultiLineString, P>>[]) {
sources = Lists.noNull(sources)
const all: Map<string, Feature<MultiLineString>> = new Map()
const all: Map<string, Feature<MultiLineString | LineString, P>> = new Map()
const currentZoom = this._zoomlevel?.data ?? 0
for (const source of sources) {
if (source.z != currentZoom) {
@ -48,10 +48,10 @@ export class LineSourceMerger extends UpdatableDynamicTileSource<
} else if (f.geometry.type === "MultiLineString") {
coordinates.push(...f.geometry.coordinates)
} else {
console.error("Invalid geometry type:", f.geometry.type)
console.error("Invalid geometry type:", f.geometry["type"])
continue
}
const oldV = all.get(id)
const oldV: Feature<MultiLineString | LineString, P> = all.get(id)
if (!oldV) {
all.set(id, {
type: "Feature",
@ -63,7 +63,13 @@ export class LineSourceMerger extends UpdatableDynamicTileSource<
})
continue
}
oldV.geometry.coordinates.push(...coordinates)
for (const coordinate of coordinates) {
if (oldV.geometry.type === "LineString") {
oldV.geometry.coordinates.push(...coordinate)
} else {
oldV.geometry.coordinates.push(coordinate)
}
}
}
}

View file

@ -2,11 +2,11 @@ import DynamicTileSource from "./DynamicTileSource"
import { ImmutableStore, Store } from "../../UIEventSource"
import { BBox } from "../../BBox"
import TileLocalStorage from "../Actors/TileLocalStorage"
import { Feature } from "geojson"
import { Feature, Geometry } from "geojson"
import StaticFeatureSource from "../Sources/StaticFeatureSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
export default class LocalStorageFeatureSource extends DynamicTileSource {
export default class LocalStorageFeatureSource<T extends Feature<Geometry, { [name: string]: any }>> extends DynamicTileSource<T> {
constructor(
backend: string,
layer: LayerConfig,
@ -30,8 +30,8 @@ export default class LocalStorageFeatureSource extends DynamicTileSource {
new ImmutableStore(zoomlevel),
layer.minzoom,
(tileIndex) =>
new StaticFeatureSource(
storage.getTileSource(tileIndex).mapD((features) => {
new StaticFeatureSource<T>(
<Store<T[]>> storage.getTileSource(tileIndex).mapD((features) => {
if (features.length === undefined) {
console.trace("These are not features:", features)
storage.invalidate(tileIndex)

View file

@ -1,7 +1,7 @@
import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource"
import { Store } from "../../UIEventSource"
import { BBox } from "../../BBox"
import { Feature } from "geojson"
import { Feature, Polygon } from "geojson"
import { GeoOperations } from "../../GeoOperations"
import { UpdatableDynamicTileSource } from "./DynamicTileSource"
import { Lists } from "../../../Utils/Lists"
@ -10,13 +10,14 @@ import { Lists } from "../../../Utils/Lists"
* The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together.
* This is used to reconstruct polygons of vector tiles
*/
export class PolygonSourceMerger extends UpdatableDynamicTileSource<
FeatureSourceForTile & UpdatableFeatureSource
export class PolygonSourceMerger<P extends Record<string, any> & { id: string },
F extends Feature<Polygon, P> = Feature<Polygon, P>> extends UpdatableDynamicTileSource<
F, FeatureSourceForTile<F> & UpdatableFeatureSource<F>
> {
constructor(
zoomlevel: Store<number>,
minzoom: number,
constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource,
constructSource: (tileIndex: number) => FeatureSourceForTile<F> & UpdatableFeatureSource<F>,
mapProperties: {
bounds: Store<BBox>
zoom: Store<number>
@ -28,9 +29,9 @@ export class PolygonSourceMerger extends UpdatableDynamicTileSource<
super(zoomlevel, minzoom, constructSource, mapProperties, options)
}
protected addDataFromSources(sources: FeatureSourceForTile[]) {
protected addDataFromSources(sources: FeatureSourceForTile<F>[]) {
sources = Lists.noNull(sources)
const all: Map<string, Feature> = new Map()
const all: Map<string, F> = new Map()
const zooms: Map<string, number> = new Map()
for (const source of sources) {
@ -60,7 +61,7 @@ export class PolygonSourceMerger extends UpdatableDynamicTileSource<
zooms.set(id, z)
continue
}
const merged = GeoOperations.union(f, oldV)
const merged = <F> GeoOperations.union(f, oldV)
merged.properties = oldV.properties
all.set(id, merged)
zooms.set(id, z)

View file

@ -54,11 +54,11 @@ export class GeoOperations {
/**
* Create a union between two features
*/
public static union(
public static union<P >(
f0: Feature<Polygon | MultiPolygon>,
f1: Feature<Polygon | MultiPolygon>
): Feature<Polygon | MultiPolygon> | null {
return turf.union(turf.featureCollection([f0, f1]))
): Feature<Polygon | MultiPolygon, P> | null {
return turf.union<P>(turf.featureCollection([f0, f1]))
}
public static intersect(

View file

@ -12,6 +12,7 @@ import CreateNewWayAction from "./CreateNewWayAction"
import ThemeConfig from "../../../Models/ThemeConfig/ThemeConfig"
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { Position } from "geojson"
import type { OsmFeature } from "../../../Models/OsmFeature"
export interface MergePointConfig {
withinRangeOfM: number
@ -71,7 +72,7 @@ export default class CreateWayWithPointReuseAction
private readonly _state: {
theme: ThemeConfig
changes: Changes
indexedFeatures: IndexedFeatureSource
indexedFeatures: IndexedFeatureSource<OsmFeature>
fullNodeDatabase?: FullNodeDatabaseSource
}
private readonly _config: MergePointConfig[]
@ -82,7 +83,7 @@ export default class CreateWayWithPointReuseAction
state: {
theme: ThemeConfig
changes: Changes
indexedFeatures: IndexedFeatureSource
indexedFeatures: IndexedFeatureSource<OsmFeature>
fullNodeDatabase?: FullNodeDatabaseSource
},
config: MergePointConfig[]
@ -199,7 +200,7 @@ export default class CreateWayWithPointReuseAction
}
features.push(newGeometry)
}
return StaticFeatureSource.fromGeojson(features)
return new StaticFeatureSource(features)
}
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {

View file

@ -14,6 +14,7 @@ import { OsmConnection } from "../OsmConnection"
import { Feature, Geometry, LineString, Point } from "geojson"
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { Lists } from "../../../Utils/Lists"
import { OsmFeature } from "../../../Models/OsmFeature"
export default class ReplaceGeometryAction extends OsmChangeAction implements PreviewableAction {
/**
@ -90,13 +91,13 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
public async getPreview(): Promise<FeatureSource> {
const { closestIds, allNodesById, detachedNodes, reprojectedNodes } =
await this.GetClosestIds()
const preview: Feature<Geometry>[] = closestIds.map((newId, i) => {
const preview: Feature<Geometry, {id: string}>[] = closestIds.map((newId, i) => {
if (this.identicalTo[i] !== undefined) {
return undefined
}
if (newId === undefined) {
return {
return <OsmFeature> {
type: "Feature",
properties: {
newpoint: "yes",
@ -127,7 +128,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
reprojectedNodes.forEach(({ newLat, newLon, nodeId }) => {
const origNode = allNodesById.get(nodeId)
const feature: Feature<LineString> = {
const feature: Feature<LineString, {id: string} & Record<string, any>> = {
type: "Feature",
properties: {
move: "yes",
@ -149,7 +150,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
detachedNodes.forEach(({ reason }, id) => {
const origNode = allNodesById.get(id)
const feature: Feature<Point> = {
const feature: OsmFeature & Feature<Point> = {
type: "Feature",
properties: {
detach: "yes",
@ -165,7 +166,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
preview.push(feature)
})
return StaticFeatureSource.fromGeojson(Lists.noNull(preview))
return new StaticFeatureSource(Lists.noNull(preview))
}
/**

View file

@ -3,9 +3,9 @@ import { Utils } from "../../Utils"
import { ImmutableStore, Store } from "../UIEventSource"
import { BBox } from "../BBox"
import osmtogeojson from "osmtogeojson"
import { FeatureCollection, Geometry } from "geojson"
import { OsmTags } from "../../Models/OsmFeature"
;("use strict")
import { Feature } from "geojson"
("use strict")
/**
* Interfaces overpass to get all the latest data
*/
@ -37,7 +37,7 @@ export class Overpass {
this._includeMeta = includeMeta
}
public async queryGeoJson(bounds: BBox): Promise<[FeatureCollection<Geometry, OsmTags>, Date]> {
public async queryGeoJson<T extends Feature>(bounds: BBox): Promise<[{features: T[]}, Date]> {
const bbox =
"[bbox:" +
bounds.getSouth() +
@ -49,16 +49,16 @@ export class Overpass {
bounds.getEast() +
"]"
const query = this.buildScript(bbox)
return await this.ExecuteQuery(query)
return await this.ExecuteQuery<T>(query)
}
public buildUrl(query: string) {
return `${this._interpreterUrl}?data=${encodeURIComponent(query)}`
}
private async ExecuteQuery(
private async ExecuteQuery<T extends Feature>(
query: string
): Promise<[FeatureCollection<Geometry, OsmTags>, Date]> {
): Promise<[{features: T[]}, Date]> {
const json = await Utils.downloadJson<{
elements: []
remark
@ -73,7 +73,7 @@ export class Overpass {
console.warn("No features for", this.buildUrl(query))
}
const geojson = <FeatureCollection<Geometry, OsmTags>>osmtogeojson(json)
const geojson = <{features: T[]}> <any> osmtogeojson(json)
const osmTime = new Date(json.osm3s.timestamp_osm_base)
return [geojson, osmTime]
}

View file

@ -357,7 +357,7 @@ export default class ShowDataLayer {
public static showMultipleLayers(
mlmap: UIEventSource<MlMap>,
features: FeatureSource,
features: FeatureSource<Feature<Geometry, Record<string, any> & {id: string}>>,
layers: LayerConfig[],
options?: Partial<Omit<ShowDataLayerOptions, "features" | "layer">>
) {

View file

@ -11,6 +11,7 @@ import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
import SvelteUIElement from "../Base/SvelteUIElement"
import { Feature } from "geojson"
import AutoApplyButton from "./AutoApplyButton.svelte"
import { OsmFeature } from "../../Models/OsmFeature"
export abstract class AutoAction extends SpecialVisualization {
supportsAutoAction: boolean
@ -20,7 +21,7 @@ export abstract class AutoAction extends SpecialVisualization {
state: {
theme: ThemeConfig
changes: Changes
indexedFeatures: IndexedFeatureSource
indexedFeatures: IndexedFeatureSource<OsmFeature>
},
tagSource: UIEventSource<Record<string, string>>,
argument: string[]

View file

@ -14,6 +14,7 @@ import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/C
import ThemeConfig from "../../../Models/ThemeConfig/ThemeConfig"
import { Changes } from "../../../Logic/Osm/Changes"
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
import { OsmFeature } from "../../../Models/OsmFeature"
export interface WayImportFlowArguments extends ImportFlowArguments {
max_snap_distance: string
@ -54,7 +55,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
state: {
theme: ThemeConfig
changes: Changes
indexedFeatures: IndexedFeatureSource
indexedFeatures: IndexedFeatureSource<OsmFeature>
fullNodeDatabase?: FullNodeDatabaseSource
},
tagsToApply: Store<Tag[]>,
@ -82,7 +83,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
const coors = feature.geometry.coordinates
return new CreateWayWithPointReuseAction(tagsToApply.data, coors, state, mergeConfigs)
} else {
throw "Unsupported type"
throw "Unsupported type: cannot import something of type "+feature.geometry["type"]+", only Polygon and LineString are supported"
}
}