Merge develop

This commit is contained in:
Pieter Vander Vennet 2025-08-26 23:52:32 +02:00
commit cac87a5467
727 changed files with 27522 additions and 15764 deletions

View file

@ -1,6 +1,10 @@
import { Store, UIEventSource } from "../UIEventSource"
import { Utils } from "../../Utils"
import { AvailableRasterLayers, RasterLayerPolygon, RasterLayerUtils } from "../../Models/RasterLayers"
import {
AvailableRasterLayers,
RasterLayerPolygon,
RasterLayerUtils,
} from "../../Models/RasterLayers"
/**
* When a user pans around on the map, they might pan out of the range of the current background raster layer.
@ -22,10 +26,7 @@ export default class BackgroundLayerResetter {
(global) => global.properties.id !== l.properties.id
)
) {
BackgroundLayerResetter.installHandler(
currentBackgroundLayer,
availableLayers
)
BackgroundLayerResetter.installHandler(currentBackgroundLayer, availableLayers)
return true // unregister
}
})

View file

@ -144,25 +144,27 @@ export default class DetermineTheme {
if (json.layers === undefined && json.tagRenderings !== undefined) {
// We got fed a layer instead of a theme
const layerConfig = <LayerConfigJson>json
let icon = Lists.noNull(layerConfig.pointRendering
.flatMap((pr) => pr.marker)
.map((iconSpec) => {
if (!iconSpec) {
return undefined
}
const icon = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.icon)
.render.txt
if (
iconSpec.color === undefined ||
icon.startsWith("http:") ||
icon.startsWith("https:")
) {
return icon
}
const color = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.color)
.render.txt
return icon + ":" + color
})).join(";")
let icon = Lists.noNull(
layerConfig.pointRendering
.flatMap((pr) => pr.marker)
.map((iconSpec) => {
if (!iconSpec) {
return undefined
}
const icon = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.icon)
.render.txt
if (
iconSpec.color === undefined ||
icon.startsWith("http:") ||
icon.startsWith("https:")
) {
return icon
}
const color = new TagRenderingConfig(<TagRenderingConfigJson>iconSpec.color)
.render.txt
return icon + ":" + color
})
).join(";")
if (!icon) {
icon = "./assets/svg/bug.svg"

View file

@ -7,21 +7,24 @@ export interface FeatureSource<T extends Feature = Feature<Geometry, OsmTags>> {
features: Store<T[]>
}
export interface UpdatableFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> extends FeatureSource<T> {
export interface UpdatableFeatureSource<T extends Feature = Feature<Geometry, OsmTags>>
extends FeatureSource<T> {
/**
* Forces an update and downloads the data, even if the feature source is supposed to be active
*/
updateAsync(): void
}
export interface WritableFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> extends FeatureSource<T> {
export interface WritableFeatureSource<T extends Feature = Feature<Geometry, OsmTags>>
extends FeatureSource<T> {
features: UIEventSource<T[]>
}
/**
* A feature source which only contains features for the defined layer
*/
export interface FeatureSourceForLayer<T extends Feature = Feature<Geometry, OsmTags>> extends FeatureSource<T> {
export interface FeatureSourceForLayer<T extends Feature = Feature<Geometry, OsmTags>>
extends FeatureSource<T> {
readonly layer: FilteredLayer
}
@ -33,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

@ -11,7 +11,10 @@ import { UIEventSource } from "../UIEventSource"
* 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<T extends Feature, SRC extends FeatureSource<T>> {
export default class PerLayerFeatureSourceSplitter<
T extends Feature,
SRC extends FeatureSource<T>
> {
public readonly perLayer: ReadonlyMap<string, SRC>
constructor(
layers: FilteredLayer[],
@ -115,7 +118,7 @@ export default class PerLayerFeatureSourceSplitter<T extends Feature, SRC extend
})
}
public forEach(f: ((src: SRC) => void)) {
public forEach(f: (src: SRC) => void) {
for (const fs of this.perLayer.values()) {
f(fs)
}

View file

@ -209,6 +209,9 @@ export default class FavouritesFeatureSource extends StaticFeatureSource {
continue
}
const store = featureProperties.getStore(id)
if (store === undefined) {
continue
}
const origValue = store.data._favourite
if (detected.indexOf(id) >= 0) {
if (origValue !== "yes") {

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

@ -5,7 +5,9 @@ import { Feature, Geometry } from "geojson"
import { GlobalFilter } from "../../../Models/GlobalFilter"
import { OsmTags } from "../../../Models/OsmFeature"
export default class FilteringFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> implements FeatureSource<T> {
export default class FilteringFeatureSource<T extends Feature = Feature<Geometry, OsmTags>>
implements FeatureSource<T>
{
public readonly features: UIEventSource<T[]> = new UIEventSource([])
private readonly upstream: FeatureSource<T>
private readonly _fetchStore?: (id: string) => Store<Record<string, string>>

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

@ -3,14 +3,12 @@ import { Feature } from "geojson"
import { Store, UIEventSource } from "../../UIEventSource"
export class IfVisibleFeatureSource<T extends Feature> implements FeatureSource<T> {
private readonly _features: UIEventSource<T[]> = new UIEventSource<T[]>([])
public readonly features: Store<T[]> = this._features
constructor(upstream: FeatureSource<T>, visible: Store<boolean>) {
let dirty = false
upstream.features.addCallbackAndRun(features => {
upstream.features.addCallbackAndRun((features) => {
if (!visible.data) {
dirty = true
this._features.set([])
@ -20,7 +18,7 @@ export class IfVisibleFeatureSource<T extends Feature> implements FeatureSource<
dirty = false
})
visible.addCallbackAndRun(isVisible => {
visible.addCallbackAndRun((isVisible) => {
if (isVisible && dirty) {
this._features.set(upstream.features.data)
dirty = false
@ -28,11 +26,6 @@ export class IfVisibleFeatureSource<T extends Feature> implements FeatureSource<
if (!visible) {
this._features.set([])
}
})
}
}

View file

@ -49,9 +49,11 @@ export class LastClickFeatureSource implements FeatureSource {
allPresets.push(html)
}
this.renderings = Lists.dedup(allPresets.map((uiElem) =>
Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML
))
this.renderings = Lists.dedup(
allPresets.map((uiElem) =>
Utils.runningFromConsole ? "" : uiElem.ConstructElement().innerHTML
)
)
this._features = new UIEventSource<Feature[]>([])
this.features = this._features

View file

@ -5,24 +5,17 @@ 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
private readonly _url: string
private readonly _features: UIEventSource<
GeojsonFeature<Geometry, OsmTags>[]
> = new UIEventSource<GeojsonFeature<Geometry, OsmTags>[]>([])
private readonly _features: UIEventSource<GeojsonFeature<Geometry, OsmTags>[]> =
new UIEventSource<GeojsonFeature<Geometry, OsmTags>[]>([])
private currentlyRunning: Promise<any>
constructor(
url: string,
x: number,
y: number,
z: number,
isActive?: Store<boolean>
) {
constructor(url: string, x: number, y: number, z: number, isActive?: Store<boolean>) {
this._url = url
this.x = x
this.y = y
@ -54,7 +47,9 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
return
}
const buffer = await result.arrayBuffer()
const features = <Feature<Geometry, OsmTags>[]>MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z)
const features = <Feature<Geometry, OsmTags>[]>(
MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z)
)
for (const feature of features) {
const properties = feature.properties
if (!properties["osm_type"]) {

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

@ -4,7 +4,9 @@ import { FeatureSourceForLayer } from "../FeatureSource"
import { Feature, Geometry } from "geojson"
import { OsmTags } from "../../../Models/OsmFeature"
export default class SimpleFeatureSource<T extends Feature = Feature<Geometry, OsmTags>> implements FeatureSourceForLayer<T> {
export default class SimpleFeatureSource<T extends Feature = Feature<Geometry, OsmTags>>
implements FeatureSourceForLayer<T>
{
public readonly features: UIEventSource<T[]>
public readonly layer: FilteredLayer

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

@ -3,6 +3,7 @@ import { Feature, Point } from "geojson"
import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource"
import { GeoOperations } from "../../GeoOperations"
import { Tiles } from "../../../Models/TileRange"
import { Lists } from "../../../Utils/Lists"
export interface ClusteringOptions {
/**
@ -19,13 +20,14 @@ export interface ClusteringOptions {
}
interface SummaryProperties {
id: string,
total: number,
id: string
total: number
tile_id: number
}
export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>> implements FeatureSource<T> {
export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>>
implements FeatureSource<T>
{
private readonly id: string
private readonly showSummaryAt: "tilecenter" | "average"
features: Store<T[]>
@ -37,52 +39,58 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>>
*
* We ignore the polygons, as polygons get smaller when zoomed out and thus don't clutter the map too much
*/
constructor(upstream: FeatureSource<T>,
currentZoomlevel: Store<number>,
id: string,
options?: ClusteringOptions) {
constructor(
upstream: FeatureSource<T>,
currentZoomlevel: Store<number>,
id: string,
options?: ClusteringOptions
) {
this.id = id
this.showSummaryAt = options?.showSummaryAt ?? "average"
const clusterCutoff = options?.dontClusterAboveZoom ?? 17
const doCluster = options?.dontClusterAboveZoom === undefined ? new ImmutableStore(true) : currentZoomlevel.map(zoom => zoom <= clusterCutoff)
const doCluster =
options?.dontClusterAboveZoom === undefined
? new ImmutableStore(true)
: currentZoomlevel.map((zoom) => zoom <= clusterCutoff)
const cutoff = options?.cutoff ?? 20
const summaryPoints = new UIEventSource<Feature<Point, SummaryProperties>[]>([])
currentZoomlevel = currentZoomlevel.stabilized(500).map(z => Math.floor(z))
this.features = (upstream.features.map(features => {
if (!doCluster.data) {
summaryPoints.set([])
return features
}
const z = currentZoomlevel.data
const perTile = GeoOperations.spreadIntoBboxes(features, z)
const resultingFeatures = []
const summary: Feature<Point, SummaryProperties>[] = []
for (const tileIndex of perTile.keys()) {
const tileFeatures: Feature<Point>[] = perTile.get(tileIndex)
if (tileFeatures.length > cutoff) {
summary.push(this.createSummaryFeature(tileFeatures, tileIndex))
} else {
resultingFeatures.push(...tileFeatures)
currentZoomlevel = currentZoomlevel.stabilized(500).map((z) => Math.floor(z))
this.features = upstream.features.map(
(features) => {
if (!doCluster.data) {
summaryPoints.set([])
return features
}
}
summaryPoints.set(summary)
return resultingFeatures
}, [doCluster, currentZoomlevel]))
const z = currentZoomlevel.data
const perTile = GeoOperations.spreadIntoBboxes(features, z)
const resultingFeatures = []
const summary: Feature<Point, SummaryProperties>[] = []
for (const tileIndex of perTile.keys()) {
const tileFeatures: Feature<Point>[] = perTile.get(tileIndex)
if (tileFeatures.length > cutoff) {
summary.push(this.createSummaryFeature(tileFeatures, tileIndex))
} else {
resultingFeatures.push(...tileFeatures)
}
}
summaryPoints.set(summary)
return resultingFeatures
},
[doCluster, currentZoomlevel]
)
ClusterGrouping.singleton.registerSource(summaryPoints)
}
private createSummaryFeature(features: Feature<Point>[], tileId: number): Feature<Point, SummaryProperties> {
let lon: number
let lat: number
const [z, x, y] = Tiles.tile_from_index(tileId)
private createSummaryFeature(
features: Feature<Point>[],
tileId: number
): Feature<Point, SummaryProperties> {
let coordinates: [number, number]
if (this.showSummaryAt === "tilecenter") {
[lon, lat] = Tiles.centerPointOf(z, x, y)
const [z, x, y] = Tiles.tile_from_index(tileId)
coordinates = Tiles.centerPointOf(z, x, y)
} else {
let lonSum = 0
let latSum = 0
@ -91,20 +99,21 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>>
lonSum += lon
latSum += lat
}
lon = lonSum / features.length
lat = latSum / features.length
const lon = lonSum / features.length
const lat = latSum / features.length
coordinates = [lon, lat]
}
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: [lon, lat]
coordinates
},
properties: {
id: "summary_" + this.id + "_" + tileId,
tile_id: tileId,
total: features.length
}
total: features.length,
},
}
}
}
@ -112,47 +121,55 @@ export class ClusteringFeatureSource<T extends Feature<Point> = Feature<Point>>
/**
* Groups multiple summaries together
*/
export class ClusterGrouping implements FeatureSource<Feature<Point, { total_metric: string }>> {
private readonly _features: UIEventSource<Feature<Point, { total_metric: string }>[]> = new UIEventSource([])
public readonly features: Store<Feature<Point, { total_metric: string }>[]> = this._features
export class ClusterGrouping implements FeatureSource<Feature<Point, { total_metric: string, id: string }>> {
private readonly _features: UIEventSource<Feature<Point, { total_metric: string, id: string }>[]> =
new UIEventSource([])
public readonly features: Store<Feature<Point, { total_metric: string, id: string }>[]> = this._features
public static readonly singleton = new ClusterGrouping()
public readonly isDirty = new UIEventSource(false)
private constructor() {
this.isDirty.stabilized(200).addCallback(dirty => {
this.isDirty.stabilized(200).addCallback((dirty) => {
if (dirty) {
this.update()
}
})
}
private allSource: Store<Feature<Point, { total: number, tile_id: number }>[]>[] = []
private allSource: Store<Feature<Point, { total: number; tile_id: number }>[]>[] = []
private update() {
const countPerTile = new Map<number, number>()
const countPerTile = new Map<number, { lon: number, lat: number, count: number }[]>()
for (const source of this.allSource) {
for (const f of source.data) {
const id = f.properties.tile_id
const count = f.properties.total + (countPerTile.get(id) ?? 0)
countPerTile.set(id, count)
if (!countPerTile.has(id)) {
countPerTile.set(id, [])
}
const ls = countPerTile.get(id)
ls.push({ lon: f.geometry.coordinates[0], lat: f.geometry.coordinates[1], count: f.properties.total })
}
}
const features: Feature<Point, { total_metric: string, id: string }>[] = []
const features: Feature<Point, { total_metric: string; id: string }>[] = []
const now = new Date().getTime() + ""
for (const tileId of countPerTile.keys()) {
const coordinates = Tiles.centerPointOf(tileId)
const data = countPerTile.get(tileId)
const total = Lists.sum(data.map(d => d.count))
const lon = Lists.sum(data.map(d => d.lon * d.count)) / total
const lat = Lists.sum(data.map(d => d.lat * d.count)) / total
features.push({
type: "Feature",
properties: {
total_metric: "" + countPerTile.get(tileId),
id: "clustered_all_" + tileId + "_" + now // We add the date to force a fresh ID every time, this makes sure values are updated
total_metric: "" + total,
id: "clustered_all_" + tileId + "_" + now, // We add the date to force a fresh ID every time, this makes sure values are updated
},
geometry: {
type: "Point",
coordinates
}
coordinates: [lon, lat]
},
})
}
this._features.set(features)
@ -160,11 +177,14 @@ export class ClusterGrouping implements FeatureSource<Feature<Point, { total_met
}
public registerSource(features: Store<Feature<Point, SummaryProperties>[]>) {
if (this.allSource.indexOf(features) >= 0) {
console.error("This source has already been registered")
return
}
this.allSource.push(features)
features.addCallbackAndRun(() => {
//this.isDirty.set(true)
this.update()
})
}
}

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

@ -20,7 +20,7 @@ export class SummaryTileSourceRewriter implements FeatureSource {
public readonly totalNumberOfFeatures: Store<number> = this._totalNumberOfFeatures
constructor(
summarySource: SummaryTileSource,
filteredLayers: ReadonlyMap<string, FilteredLayer>,
filteredLayers: ReadonlyMap<string, FilteredLayer>
) {
this.filteredLayers = Array.from(filteredLayers.values()).filter(
(l) => !Constants.isPriviliged(l.layerDef)
@ -73,18 +73,20 @@ export class SummaryTileSource extends DynamicTileSource {
zoom: Store<number>
},
options?: {
isActive?: Store<boolean>,
isActive?: Store<boolean>
availableLayers?: Store<Set<string>>
}
) {
const zDiff = 2
const layersSummed = (options?.availableLayers??new ImmutableStore(undefined)).map(available => {
console.log("Determining 'layersSummed' with currently available:", available)
if(available === undefined){
return layers.join("+")
const layersSummed = (options?.availableLayers ?? new ImmutableStore(undefined)).map(
(available) => {
console.log("Determining 'layersSummed' with currently available:", available)
if (available === undefined) {
return layers.join("+")
}
return layers.filter((l) => available.has(l)).join("+")
}
return layers.filter(l => available.has(l)).join("+")
})
)
super(
zoomRounded,
0, // minzoom
@ -117,7 +119,7 @@ export class SummaryTileSource extends DynamicTileSource {
cacheserver: string,
layersSummed: string
): Store<Feature<Point>[]> {
if(layersSummed === ""){
if (layersSummed === "") {
return new ImmutableStore([])
}
const [z, x, y] = Tiles.tile_from_index(tileIndex)

View file

@ -10,13 +10,12 @@ import {
MultiPolygon,
Point,
Polygon,
Position
Position,
} from "geojson"
import { Tiles } from "../Models/TileRange"
import { Utils } from "../Utils"
import { Lists } from "../Utils/Lists"
("use strict")
;("use strict")
export class GeoOperations {
private static readonly _earthRadius: number = 6378137
@ -54,11 +53,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(
@ -538,7 +537,10 @@ export class GeoOperations {
* @param features
* @param zoomlevel
*/
public static spreadIntoBboxes<T extends Feature = Feature>(features: T[], zoomlevel: number): Map<number, T[]> {
public static spreadIntoBboxes<T extends Feature = Feature>(
features: T[],
zoomlevel: number
): Map<number, T[]> {
const perBbox = new Map<number, T[]>()
const z = zoomlevel
for (const feature of features) {

View file

@ -6,7 +6,6 @@ import { ImmutableStore, Store, Stores } from "../UIEventSource"
import ImageProvider, { ProvidedImage } from "./ImageProvider"
import { WikidataImageProvider } from "./WikidataImageProvider"
import Panoramax from "./Panoramax"
import { Utils } from "../../Utils"
import { ServerSourceInfo } from "../../Models/SourceOverview"
import { Lists } from "../../Utils/Lists"
@ -102,7 +101,9 @@ export default class AllImageProviders {
Mapillary.singleton,
AllImageProviders.genericImageProvider,
]
const allPrefixes = Lists.dedup(prefixes ?? [].concat(...sources.map((s) => s.defaultKeyPrefixes)))
const allPrefixes = Lists.dedup(
prefixes ?? [].concat(...sources.map((s) => s.defaultKeyPrefixes))
)
for (const prefix of allPrefixes) {
for (const k in tags) {
const v = tags[k]
@ -148,8 +149,8 @@ export default class AllImageProviders {
allSources.push(singleSource)
}
const source = Stores.concat(allSources).map((result) => {
const all = result.flatMap(x => x)
return Utils.DedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id])
const all = result.flatMap((x) => x)
return Lists.dedupOnId(all, (i) => [i?.id, i?.url, i?.alt_id])
})
this._cachedImageStores[cachekey] = source
return source

View file

@ -14,7 +14,6 @@ import { GeoOperations } from "../GeoOperations"
import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import ExifReader from "exifreader"
import { Utils } from "../../Utils"
import { Lists } from "../../Utils/Lists"
import { IsOnline } from "../Web/IsOnline"
@ -219,6 +218,8 @@ export class ImageUploadManager {
let result: UploadResult = undefined
let attempts = 2
// Remove from 'failed' to reset the UI counters
this._fails.set(this._fails.data.filter((a) => a !== args))
while (attempts > 0 && result === undefined) {
attempts--
const doReport = attempts == 0
@ -232,7 +233,6 @@ export class ImageUploadManager {
}
}
this._isUploading.set(undefined)
this._fails.set(this._fails.data.filter((a) => a !== args))
if (result === undefined) {
this._fails.data.push(args)
this._fails.ping()

View file

@ -75,9 +75,11 @@ export class Mapillary extends ImageProvider {
pKey,
}
const baselink = `https://www.mapillary.com/app/?`
const paramsStr = Lists.noNull(Object.keys(params).map((k) =>
params[k] === undefined ? undefined : k + "=" + params[k]
))
const paramsStr = Lists.noNull(
Object.keys(params).map((k) =>
params[k] === undefined ? undefined : k + "=" + params[k]
)
)
return baselink + paramsStr.join("&")
}

View file

@ -11,6 +11,9 @@ import { Feature, Point } from "geojson"
import { AddImageOptions } from "panoramax-js/dist/Panoramax"
import { ServerSourceInfo } from "../../Models/SourceOverview"
import { ComponentType } from "svelte/types/runtime/internal/dev"
import { Strings } from "../../Utils/Strings"
import { Utils } from "../../Utils"
import { Lists } from "../../Utils/Lists"
export default class PanoramaxImageProvider extends ImageProvider {
public static readonly singleton: PanoramaxImageProvider = new PanoramaxImageProvider()
@ -194,9 +197,13 @@ export default class PanoramaxImageProvider extends ImageProvider {
public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> {
const meta = await this.getInfoFor(providedImage.id)
const artists = Lists.noEmpty(meta.data.providers.map(p => p.name))
// We take the last provider, as that one probably contain the username of the uploader
const artist = artists.at(-1)
return {
artist: meta.data.providers.at(-1).name, // We take the last provider, as that one probably contain the username of the uploader
artist,
date: new Date(meta.data.properties["datetime"]),
licenseShortName: meta.data.properties["geovisio:license"],
}

View file

@ -5,7 +5,6 @@ import Constants from "../Models/Constants"
import { Store, UIEventSource } from "./UIEventSource"
import { Utils } from "../Utils"
export interface AreaDescription {
/**
* Thie filename at the host and in the indexedDb
@ -15,16 +14,16 @@ export interface AreaDescription {
/**
* Minzoom that is covered, inclusive
*/
minzoom: number,
minzoom: number
/**
* Maxzoom that is covered, inclusive
*/
maxzoom: number,
maxzoom: number
/**
* The x, y of the tile that is covered (at minzoom)
*/
x: number,
y: number,
x: number
y: number
/**
* ISO-datestring of when the data was processed
@ -35,7 +34,6 @@ export interface AreaDescription {
* Blob.size
*/
size?: number
}
class TypedIdb<T> {
@ -52,7 +50,6 @@ class TypedIdb<T> {
return Promise.resolve(undefined)
}
return new Promise((resolve, reject) => {
const request: IDBOpenDBRequest = indexedDB.open(name)
request.onerror = (event) => {
console.error("Could not open the Database: ", event)
@ -68,7 +65,6 @@ class TypedIdb<T> {
})
}
async set(key: string, value: T): Promise<void> {
if (Utils.runningFromConsole) {
return Promise.resolve()
@ -134,8 +130,6 @@ class TypedIdb<T> {
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}
}
@ -153,7 +147,7 @@ class BlobSource implements Source {
async getBytes(offset: number, length: number): Promise<RangeResponse> {
const sliced = this._blob.slice(offset, offset + length)
return {
data: await sliced.arrayBuffer()
data: await sliced.arrayBuffer(),
}
}
@ -168,9 +162,7 @@ class BlobSource implements Source {
async getDataVersion(): Promise<string> {
const meta: Record<string, string> = <any>await this.pmtiles.getMetadata()
return meta["planetiler:osm:osmosisreplicationtime"]
}
}
export class OfflineBasemapManager {
@ -184,7 +176,7 @@ export class OfflineBasemapManager {
0: 4,
5: 7,
8: 9,
10: undefined
10: undefined,
} as const
private readonly blobs: TypedIdb<any>
@ -192,7 +184,9 @@ export class OfflineBasemapManager {
public _installedAreas: UIEventSource<AreaDescription[]> = new UIEventSource([])
public installedAreas: Store<ReadonlyArray<Readonly<AreaDescription>>> = this._installedAreas
private readonly _installing: UIEventSource<Map<string, Promise<boolean>>> = new UIEventSource(new Map())
private readonly _installing: UIEventSource<Map<string, Promise<boolean>>> = new UIEventSource(
new Map()
)
public readonly installing: Store<ReadonlyMap<string, object>> = this._installing
public static singleton = new OfflineBasemapManager(Constants.pmtiles_host)
@ -224,18 +218,25 @@ export class OfflineBasemapManager {
return this.installedAreas.data
}
public isInstalled(toCompare: { z?: number, minzoom?: number, x: number, y: number }): boolean {
return this.installedAreas.data.some(area => area.x === toCompare.x && area.y === toCompare.y && (toCompare.minzoom ?? toCompare.z) === area.minzoom)
public isInstalled(toCompare: { z?: number; minzoom?: number; x: number; y: number }): boolean {
return this.installedAreas.data.some(
(area) =>
area.x === toCompare.x &&
area.y === toCompare.y &&
(toCompare.minzoom ?? toCompare.z) === area.minzoom
)
}
/**
* Returns all AreaDescriptions needed for the specified tile. Most specific zoom level last.
* Already installed area descriptions are _not_ returned
* @param tile
*/
public* getInstallCandidates(tile: { z: number, x: number, y: number }): Generator<AreaDescription, void, unknown> {
public *getInstallCandidates(tile: {
z: number
x: number
y: number
}): Generator<AreaDescription, void, unknown> {
for (const k in OfflineBasemapManager.zoomelevels) {
const z = Number(k)
@ -250,12 +251,11 @@ export class OfflineBasemapManager {
name: `${z}-${x}-${y}.pmtiles`,
minzoom: z,
maxzoom: OfflineBasemapManager.zoomelevels[z] ?? 15,
x, y
x,
y,
}
}
}
}
/**
@ -276,7 +276,10 @@ export class OfflineBasemapManager {
}
const blob = await response.blob()
await this.blobs.set(areaDescription.name, blob)
areaDescription.dataVersion = await new BlobSource(areaDescription.name, blob).getDataVersion()
areaDescription.dataVersion = await new BlobSource(
areaDescription.name,
blob
).getDataVersion()
areaDescription.size = blob.size
await this.meta.set(areaDescription.name, areaDescription)
await this.updateCachedMeta()
@ -289,17 +292,20 @@ export class OfflineBasemapManager {
* @see GeneratePmTilesExtractionScript
*/
public static getAreaDescriptionForMapcomplete(name: string): AreaDescription {
if (!name.endsWith(".pmtiles")) {
throw "Invalid filename, should end with .pmtiles"
}
const [z, x, y] = name.substring(0, name.length - ".pmtiles".length).split("-").map(Number)
const [z, x, y] = name
.substring(0, name.length - ".pmtiles".length)
.split("-")
.map(Number)
const maxzooms: Record<number, number | undefined> = this.zoomelevels
return {
name,
minzoom: z,
maxzoom: maxzooms[z] ?? 15,
x, y
x,
y,
}
}
@ -342,14 +348,16 @@ export class OfflineBasemapManager {
if (alreadyInstalling) {
return alreadyInstalling
}
const promise = this.installArea(candidate).catch(e => {
console.error("Could not install basemap archive", candidate.name, "due to", e)
const promise = this.installArea(candidate)
.catch((e) => {
console.error("Could not install basemap archive", candidate.name, "due to", e)
return false
}).finally(() => {
this._installing.data.delete(candidate.name)
this._installing.ping()
})
return false
})
.finally(() => {
this._installing.data.delete(candidate.name)
this._installing.ping()
})
this._installing.data.set(candidate.name, promise)
this._installing.ping()
return promise
@ -359,7 +367,7 @@ export class OfflineBasemapManager {
* Attempts to install all required areas for the given location
* @param tile
*/
public async autoInstall(tile: { z: number, x: number, y: number }) {
public async autoInstall(tile: { z: number; x: number; y: number }) {
const candidates = this.getInstallCandidates(tile)
for (const candidate of candidates) {
await this.attemptInstall(candidate)
@ -390,13 +398,9 @@ export class OfflineBasemapManager {
console.log("Not found in the archives:", { z, x, y })
return undefined
}
console.log("Served tile", { z, x, y }, "from installed archive")
return new Response(
tileData.data,
{
headers: { "Content-Type": "application/x.protobuf" }
}
)
return new Response(tileData.data, {
headers: { "Content-Type": "application/x.protobuf" },
})
}
deleteArea(description: AreaDescription): Promise<ReadonlyArray<Readonly<AreaDescription>>> {
@ -405,12 +409,9 @@ export class OfflineBasemapManager {
return this.updateCachedMeta()
}
private async fallback(params: RequestParameters,
abortController: AbortController) {
private async fallback(params: RequestParameters, abortController: AbortController) {
params.url = params.url.substr("pmtilesoffl://".length)
const response = await fetch(
new Request(params.url, params)
, abortController)
const response = await fetch(new Request(params.url, params), abortController)
if (!response.ok) {
throw new Error("Could not fetch " + params.url + "; status code is" + response.status)
}
@ -420,11 +421,17 @@ export class OfflineBasemapManager {
public async tilev4(
params: RequestParameters,
abortController: AbortController
): Promise<{ data: unknown } | { data: { tiles: string[], minzoom: number, maxzoom: number, bounds: number[] } } | {
data: Uint8Array,
cacheControl: string,
expires: string
} | { data: Uint8Array } | { data: null }> {
): Promise<
| { data: unknown }
| { data: { tiles: string[]; minzoom: number; maxzoom: number; bounds: number[] } }
| {
data: Uint8Array
cacheControl: string
expires: string
}
| { data: Uint8Array }
| { data: null }
> {
if (params.type === "arrayBuffer") {
const re = new RegExp(/(\d+)\/(\d+)\/(\d+).(mvt|pbf)/)
const result = params.url.match(re)
@ -438,9 +445,7 @@ export class OfflineBasemapManager {
if (r?.ok) {
return { data: await r.arrayBuffer() }
}
}
return await this.fallback(params, abortController)
}
}

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

@ -51,11 +51,11 @@ export default class DeleteAction extends OsmChangeAction {
} else {
this._softDeletionTags = new And(
Lists.noNull([
softDeletionTags,
new Tag(
"fixme",
`A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`
),
softDeletionTags,
new Tag(
"fixme",
`A mapcomplete user marked this feature to be deleted (${meta.specialMotivation})`
),
])
)
}

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))
}
/**
@ -318,7 +319,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
candidate = undefined
moveDistance = Infinity
distances.forEach((distances, nodeId) => {
const minDist = Math.min(...(Lists.noNull(distances)))
const minDist = Math.min(...Lists.noNull(distances))
if (moveDistance > minDist) {
// We have found a candidate to move
candidate = nodeId

View file

@ -723,9 +723,11 @@ export class Changes {
* We _do not_ pass in the Changes object itself - we want the data from OSM directly in order to apply the changes
*/
const downloader = new OsmObjectDownloader(this.backend, undefined)
const osmObjects = Lists.noNull(await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>(
neededIds.map((id) => this.getOsmObject(id, downloader))
))
const osmObjects = Lists.noNull(
await Promise.all<{ id: string; osmObj: OsmObject | "deleted" }>(
neededIds.map((id) => this.getOsmObject(id, downloader))
)
)
// Drop changes to deleted items
for (const { osmObj, id } of osmObjects) {
@ -820,21 +822,23 @@ export class Changes {
}
}
const perBinMessage = Lists.noNull(perBinCount.map((count, i) => {
if (count === 0) {
return undefined
}
const maxD = maxDistances[i]
let key = `change_within_${maxD}m`
if (maxD === Number.MAX_VALUE) {
key = `change_over_${maxDistances[i - 1]}m`
}
return {
key,
value: count,
aggregate: true,
}
}))
const perBinMessage = Lists.noNull(
perBinCount.map((count, i) => {
if (count === 0) {
return undefined
}
const maxD = maxDistances[i]
let key = `change_within_${maxD}m`
if (maxD === Number.MAX_VALUE) {
key = `change_over_${maxDistances[i - 1]}m`
}
return {
key,
value: count,
aggregate: true,
}
})
)
// This method is only called with changedescriptions for this theme
const theme = pending[0].meta.theme

View file

@ -2,7 +2,7 @@ import { Utils } from "../../Utils"
import polygon_features from "../../assets/polygon-features.json"
import { OsmFeature, OsmId, OsmTags, WayId } from "../../Models/OsmFeature"
import OsmToGeoJson from "osmtogeojson"
import { Feature, LineString, Polygon } from "geojson"
import { Feature, LineString, Point, Polygon } from "geojson"
import Constants from "../../Models/Constants"
export abstract class OsmObject {
@ -131,7 +131,7 @@ export abstract class OsmObject {
*/
public abstract centerpoint(): [number, number]
public abstract asGeoJson(): any
public abstract asGeoJson(): OsmFeature
abstract SaveExtraData(element: any, allElements: OsmObject[] | any)
@ -228,7 +228,7 @@ ${tags} </node>
return [this.lat, this.lon]
}
asGeoJson(): OsmFeature {
asGeoJson(): Feature<Point, OsmTags> {
return {
type: "Feature",
properties: this.tags,
@ -305,7 +305,7 @@ ${nds}${tags} </way>
this.nodes = element.nodes
}
public asGeoJson(): Feature<Polygon | LineString> & { properties: { id: WayId } } {
public asGeoJson(): Feature<Polygon | LineString, OsmTags> & { properties: { id: WayId } } {
const coordinates: [number, number][] | [number, number][][] = this.coordinates.map(
([lat, lon]) => [lon, lat]
)
@ -384,7 +384,7 @@ ${members}${tags} </relation>
this.geojson = geojson
}
asGeoJson(): any {
asGeoJson(): OsmFeature {
if (this.geojson !== undefined) {
return this.geojson
}

View file

@ -2,24 +2,23 @@
* Various tools for the OSM wiki
*/
export default class OsmWiki {
/**
* Create a link to the wiki for the given key and value (optional)
* @param key
* @param value
*/
public static constructLink(key: string, value?: string) : string{
public static constructLink(key: string, value?: string): string {
if (value !== undefined) {
return `https://wiki.openstreetmap.org/wiki/Tag:${key}%3D${value}`
}
return "https://wiki.openstreetmap.org/wiki/Key:" + key
}
public static constructLinkMd(key: string, value?: string) : string {
public static constructLinkMd(key: string, value?: string): string {
const link = this.constructLink(key, value)
let displayed = key
if(value){
displayed += "="+value
if (value) {
displayed += "=" + value
}
return `[${displayed}](${link})`
}

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

@ -9,7 +9,6 @@ export default class CombinedSearcher implements GeocodingProvider {
private _providersWithSuggest: ReadonlyArray<GeocodingProvider>
public readonly needsInternet
/**
* Merges the various providers together; ignores errors.
* IF all providers fail, no errors will be given
@ -18,7 +17,7 @@ export default class CombinedSearcher implements GeocodingProvider {
constructor(...providers: ReadonlyArray<GeocodingProvider>) {
this._providers = Lists.noNull(providers)
this._providersWithSuggest = this._providers.filter((pr) => pr.suggest !== undefined)
this.needsInternet = this._providers.some(p => p.needsInternet)
this.needsInternet = this._providers.some((p) => p.needsInternet)
}
/**
@ -54,10 +53,15 @@ export default class CombinedSearcher implements GeocodingProvider {
return CombinedSearcher.merge(results)
}
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]}> {
suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] }> {
const concatted = Stores.concat(
this._providersWithSuggest.map((pr) => <Store<GeocodeResult[]>> pr.suggest(query, options).map(result => result["success"] ?? []))
);
return concatted.map(gcrss => ({success: CombinedSearcher.merge(gcrss) }))
this._providersWithSuggest.map(
(pr) =>
<Store<GeocodeResult[]>>(
pr.suggest(query, options).map((result) => result["success"] ?? [])
)
)
)
return concatted.map((gcrss) => ({ success: CombinedSearcher.merge(gcrss) }))
}
}

View file

@ -77,7 +77,9 @@ export default class CoordinateSearch implements GeocodingProvider {
(m) => CoordinateSearch.asResult(m[2], m[1], "latlon")
)
const matchesLonLat = Lists.noNull(CoordinateSearch.lonLatRegexes.map((r) => query.match(r))).map((m) => CoordinateSearch.asResult(m[1], m[2], "lonlat"))
const matchesLonLat = Lists.noNull(
CoordinateSearch.lonLatRegexes.map((r) => query.match(r))
).map((m) => CoordinateSearch.asResult(m[1], m[2], "lonlat"))
const init = matches.concat(matchesLonLat)
if (init.length > 0) {
return init
@ -119,8 +121,8 @@ export default class CoordinateSearch implements GeocodingProvider {
}
}
suggest(query: string): Store<{success: GeocodeResult[]}> {
return new ImmutableStore({success: this.directSearch(query)})
suggest(query: string): Store<{ success: GeocodeResult[] }> {
return new ImmutableStore({ success: this.directSearch(query) })
}
async search(query: string): Promise<GeocodeResult[]> {

View file

@ -58,7 +58,10 @@ export default interface GeocodingProvider {
*/
search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]>
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error :any}>
suggest(
query: string,
options?: GeocodingOptions
): Store<{ success: GeocodeResult[] } | { error: any }>
}
export type ReverseGeocodingResult = Feature<
@ -90,7 +93,7 @@ export class GeocodingUtils {
// We are resetting the layeroverview; trying to parse is useless
return undefined
}
return new LayerConfig(<LayerConfigJson><any> search, "search")
return new LayerConfig(<LayerConfigJson>(<any>search), "search")
}
public static categoryToZoomLevel: Record<GeocodingCategory, number> = {

View file

@ -45,12 +45,12 @@ export default class LocalElementSearch implements GeocodingProvider {
for (const feature of features) {
const props = feature.properties
const searchTerms: string[] = Lists.noNull([
props.name,
props.alt_name,
props.local_name,
props["addr:street"] && props["addr:number"]
? props["addr:street"] + props["addr:number"]
: undefined,
props.name,
props.alt_name,
props.local_name,
props["addr:street"] && props["addr:number"]
? props["addr:street"] + props["addr:number"]
: undefined,
])
let levehnsteinD: number
@ -144,7 +144,7 @@ export default class LocalElementSearch implements GeocodingProvider {
})
}
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]}> {
return this.searchEntries(query, options, true).mapD(r => ({success:r}))
suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] }> {
return this.searchEntries(query, options, true).mapD((r) => ({ success: r }))
}
}

View file

@ -40,7 +40,10 @@ export class NominatimGeocoding implements GeocodingProvider {
return Utils.downloadJson(url)
}
suggest(query: string, options?: GeocodingOptions): Store<{ success: GeocodeResult[] } | { error: any }> {
suggest(
query: string,
options?: GeocodingOptions
): Store<{ success: GeocodeResult[] } | { error: any }> {
return UIEventSource.fromPromiseWithErr(this.search(query, options))
}
}

View file

@ -93,7 +93,10 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
return [await this.getInfoAbout(id)]
}
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> {
suggest(
query: string,
options?: GeocodingOptions
): Store<{ success: GeocodeResult[] } | { error: any }> {
return UIEventSource.fromPromiseWithErr(this.search(query, options))
}
}

View file

@ -5,7 +5,7 @@ import GeocodingProvider, {
GeocodingOptions,
GeocodingUtils,
ReverseGeocodingProvider,
ReverseGeocodingResult
ReverseGeocodingResult,
} from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { Feature, FeatureCollection } from "geojson"
@ -26,16 +26,12 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
private readonly ignoreBounds: boolean
private readonly searchLimit: number = 1
constructor(
ignoreBounds: boolean = false,
searchLimit: number = 1,
endpoint?: string
) {
constructor(ignoreBounds: boolean = false, searchLimit: number = 1, endpoint?: string) {
this.ignoreBounds = ignoreBounds
this.searchLimit = searchLimit
this._endpoint = endpoint ?? Constants.photonEndpoint ?? "https://photon.komoot.io/"
if(this.ignoreBounds){
if (this.ignoreBounds) {
this.name += " (global)"
}
}
@ -71,7 +67,10 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
return `&lang=${language}`
}
suggest(query: string, options?: GeocodingOptions): Store<{success: GeocodeResult[]} | {error: any}> {
suggest(
query: string,
options?: GeocodingOptions
): Store<{ success: GeocodeResult[] } | { error: any }> {
return UIEventSource.fromPromiseWithErr(this.search(query, options))
}

View file

@ -13,16 +13,22 @@ import { Lists } from "../../Utils/Lists"
export class ThemeSearchIndex {
private readonly themeIndex: Fuse<MinimalThemeInformation>
private readonly layerIndex: Fuse<{ id: string; description }>
/**
* A 'search' will also search matching layers from the official themes.
* This might cause themes to show up which weren't passed in 'themesToSearch', so we create a whitelist
*/
private readonly themeWhitelist: Set<string>
constructor(
language: string,
themesToSearch?: MinimalThemeInformation[],
themesToSearch: MinimalThemeInformation[],
layersToIgnore: string[] = []
) {
const themes = Utils.noNull(themesToSearch ?? ThemeSearch.officialThemes?.themes)
const themes = Utils.noNull(themesToSearch)
if (!themes) {
throw "No themes loaded. Did generate:layeroverview fail?"
}
this.themeWhitelist = new Set(themes.map(th => th.id))
const fuseOptions: IFuseOptions<MinimalThemeInformation> = {
ignoreLocation: true,
threshold: 0.2,
@ -82,6 +88,10 @@ export class ThemeSearchIndex {
const matchingThemes = ThemeSearch.layersToThemes.get(layer.item.id)
const score = layer.score
matchingThemes?.forEach((th) => {
if (!this.themeWhitelist.has(th.id)) {
// This theme was not in the 'themesToSearch'
return
}
const previous = result.get(th.id) ?? 10000
result.set(th.id, Math.min(previous, score * 5))
})

View file

@ -107,7 +107,7 @@ export class GeoLocationState {
this.requestPermission()
}
const hasLocation: Store<boolean> = this.currentGPSLocation.map(l => l !== undefined)
const hasLocation: Store<boolean> = this.currentGPSLocation.map((l) => l !== undefined)
this.gpsStateExplanation = this.gpsAvailable.map(
(available) => {
if (hasLocation.data) {

View file

@ -64,53 +64,70 @@ export default class SearchState {
return undefined
}
return this.locationSearchers
.filter(ls => !ls.needsInternet || IsOnline.isOnline.data)
.filter((ls) => !ls.needsInternet || IsOnline.isOnline.data)
.map((ls) => ({
source: ls,
results: ls.suggest(search, { bbox: bounds.data }),
}))
source: ls,
results: ls.suggest(search, { bbox: bounds.data }),
}))
},
[bounds]
)
const suggestionsList = suggestionsListWithSource
.mapD(list => list.map(sugg => sugg.results))
const suggestionsList = suggestionsListWithSource.mapD((list) =>
list.map((sugg) => sugg.results)
)
const isRunningPerEngine: Store<Store<GeocodingProvider>[]> =
suggestionsListWithSource.mapD(
allProviders => allProviders.map(provider =>
provider.results.map(result => {
suggestionsListWithSource.mapD((allProviders) =>
allProviders.map((provider) =>
provider.results.map((result) => {
if (result === undefined) {
return provider.source
} else {
return undefined
}
})))
this.runningEngines = isRunningPerEngine.bindD(
listOfSources => Stores.concat(listOfSources).mapD(list => Lists.noNull(list)))
})
)
)
this.runningEngines = isRunningPerEngine.bindD((listOfSources) =>
Stores.concat(listOfSources).mapD((list) => Lists.noNull(list))
)
this.failedEngines = suggestionsListWithSource
.bindD((allProviders: {
source: GeocodingProvider;
results: Store<{ success: GeocodeResult[] } | { error: any }>
}[]) => Stores.concat(
allProviders.map(providerAndResult =>
<Store<{ source: GeocodingProvider, error: any }[]>>providerAndResult.results.map(result => {
let error = result?.["error"]
if (error) {
return [{
source: providerAndResult.source, error,
}]
} else {
return []
}
}),
))).map(list => Lists.noNull(list?.flatMap(x => x) ?? []))
.bindD(
(
allProviders: {
source: GeocodingProvider
results: Store<{ success: GeocodeResult[] } | { error: any }>
}[]
) =>
Stores.concat(
allProviders.map(
(providerAndResult) =>
<Store<{ source: GeocodingProvider; error: any }[]>>(
providerAndResult.results.map((result) => {
let error = result?.["error"]
if (error) {
return [
{
source: providerAndResult.source,
error,
},
]
} else {
return []
}
})
)
)
)
)
.map((list) => Lists.noNull(list?.flatMap((x) => x) ?? []))
this.suggestionsSearchRunning = this.runningEngines.map(running => running?.length > 0)
this.suggestionsSearchRunning = this.runningEngines.map((running) => running?.length > 0)
this.suggestions = suggestionsList.bindD((suggestions) =>
Stores.concat(suggestions.map(sugg => sugg.map(maybe => maybe?.["success"])))
.map((suggestions: GeocodeResult[][]) => CombinedSearcher.merge(suggestions))
Stores.concat(suggestions.map((sugg) => sugg.map((maybe) => maybe?.["success"]))).map(
(suggestions: GeocodeResult[][]) => CombinedSearcher.merge(suggestions)
)
)
const themeSearch = ThemeSearchIndex.fromState(state)

View file

@ -21,6 +21,7 @@ import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeocodeResult } from "../Search/GeocodingProvider"
import Translations from "../../UI/i18n/Translations"
import { Lists } from "../../Utils/Lists"
import { AndroidPolyfill } from "../Web/AndroidPolyfill"
class RoundRobinStore<T> {
private readonly _store: UIEventSource<T[]>
@ -55,12 +56,12 @@ class RoundRobinStore<T> {
*/
public add(t: T) {
const i = this._index.data ?? 0
if(isNaN(Number(i))){
if (isNaN(Number(i))) {
this._index.set(0)
this.add(t)
return
}
this._index.set((Math.max(i,0) + 1) % this._maxCount)
this._index.set((Math.max(i, 0) + 1) % this._maxCount)
this._store.data[i] = t
this._store.ping()
}
@ -90,10 +91,12 @@ export class OptionallySyncedHistory<T extends object | string> {
defaultValue: "sync",
})
this.syncedBackingStore = UIEventSource.concat(Utils.timesT(maxHistory, (i) => {
const pref = osmconnection.getPreference(key + "-hist-" + i + "-")
return UIEventSource.asObject<T>(pref, undefined)
}))
this.syncedBackingStore = UIEventSource.concat(
Utils.timesT(maxHistory, (i) => {
const pref = osmconnection.getPreference(key + "-hist-" + i + "-")
return UIEventSource.asObject<T>(pref, undefined)
})
)
const ringIndex = UIEventSource.asInt(
osmconnection.getPreference(key + "-hist-round-robin", {
@ -373,7 +376,7 @@ export default class UserRelatedState {
private static initUserSettingsState(): LayerConfig {
try {
return new LayerConfig(<LayerConfigJson><any>usersettings, "userinformationpanel")
return new LayerConfig(<LayerConfigJson>(<any>usersettings), "userinformationpanel")
} catch (e) {
return undefined
}
@ -556,25 +559,27 @@ export default class UserRelatedState {
const untranslated = missing.untranslated.get(language) ?? []
const hasMissingTheme = untranslated.some((k) => k.startsWith("themes:"))
const missingLayers = Lists.dedup(untranslated
.filter((k) => k.startsWith("layers:"))
.map((k) => k.slice("layers:".length).split(".")[0]))
const missingLayers = Lists.dedup(
untranslated
.filter((k) => k.startsWith("layers:"))
.map((k) => k.slice("layers:".length).split(".")[0])
)
const zenLinks: { link: string; id: string }[] = Lists.noNull([
hasMissingTheme
? {
id: "theme:" + layout.id,
link: Translations.hrefToWeblateZen(
language,
"themes",
layout.id
),
}
: undefined,
...missingLayers.map((id) => ({
id: "layer:" + id,
link: Translations.hrefToWeblateZen(language, "layers", id),
})),
hasMissingTheme
? {
id: "theme:" + layout.id,
link: Translations.hrefToWeblateZen(
language,
"themes",
layout.id
),
}
: undefined,
...missingLayers.map((id) => ({
id: "layer:" + id,
link: Translations.hrefToWeblateZen(language, "layers", id),
})),
])
const untranslated_count = untranslated.length
amendedPrefs.data["_translation_total"] = "" + total
@ -673,6 +678,21 @@ export default class UserRelatedState {
amendedPrefs.ping()
})
if (!Utils.runningFromConsole) {
amendedPrefs.data["___device_pixel_ratio"] = "" + window.devicePixelRatio
}
AndroidPolyfill.getInsetSizes().top.addCallbackAndRun(
(topInset) => (amendedPrefs.data["___device_inset_top"] = "" + topInset)
)
AndroidPolyfill.getInsetSizes().bottom.addCallbackAndRun(
(topInset) => (amendedPrefs.data["___device_inset_bottom"] = "" + topInset)
)
AndroidPolyfill.inAndroid.addCallbackAndRun((isAndroid) => {
amendedPrefs.data["___device_is_android"] = "" + isAndroid
})
return amendedPrefs
}

View file

@ -1,14 +1,42 @@
import { Utils } from "../../Utils"
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
export class ThemeMetaTagging {
public static readonly themeName = "usersettings"
public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
feat.properties['__current_backgroun'] = 'initial_value'
}
}
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
feat.properties._description
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
?.at(1)
)
Utils.AddLazyProperty(
feat.properties,
"_d",
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
}
}

View file

@ -114,7 +114,7 @@ export class Tag extends TagsFilter {
return "<span class='line-through'>" + this.key + "</span>"
}
if (linkToWiki) {
const hrefK = OsmWiki.constructLink(this.key)
const hrefK = OsmWiki.constructLink(this.key)
const hrefKV = OsmWiki.constructLink(this.key, this.value)
return (
`<a href='${hrefK}' target='_blank'>${this.key}</a>` +

View file

@ -920,10 +920,10 @@ export class TagUtils {
public static GetPopularity(tag: TagsFilter): number | undefined {
if (tag instanceof And) {
return Math.min(...(Lists.noNull(tag.and.map((t) => TagUtils.GetPopularity(t))))) - 1
return Math.min(...Lists.noNull(tag.and.map((t) => TagUtils.GetPopularity(t)))) - 1
}
if (tag instanceof Or) {
return Math.max(...(Lists.noNull(tag.or.map((t) => TagUtils.GetPopularity(t))))) + 1
return Math.max(...Lists.noNull(tag.or.map((t) => TagUtils.GetPopularity(t)))) + 1
}
if (tag instanceof Tag) {
return TagUtils.GetCount(tag.key, tag.value)

View file

@ -23,12 +23,10 @@ export class Stores {
return source
}
public static concat<T>(stores: ReadonlyArray<Store<T | undefined>>): Store<(T | undefined)[]> ;
public static concat<T>(stores: ReadonlyArray<Store<T>>): Store<T[]> ;
public static concat<T>(stores: ReadonlyArray<Store<T | undefined>>): Store<(T | undefined)[]>
public static concat<T>(stores: ReadonlyArray<Store<T>>): Store<T[]>
public static concat<T>(stores: ReadonlyArray<Store<T | undefined>>): Store<(T | undefined)[]> {
const newStore = new UIEventSource<(T | undefined)[]>(
stores.map(store => store?.data),
)
const newStore = new UIEventSource<(T | undefined)[]>(stores.map((store) => store?.data))
function update() {
if (newStore._callbacks.isDestroyed) {
@ -89,6 +87,8 @@ export class Stores {
})
return newStore
}
}
export abstract class Store<T> implements Readable<T> {
@ -118,22 +118,22 @@ export abstract class Store<T> implements Readable<T> {
abstract map<J>(
f: (t: T) => J,
extraStoresToWatch: Store<unknown>[],
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J>
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<unknown>[],
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J>
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J>
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraStoresToWatch?: Store<unknown>[] | ((f: () => void) => void),
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J> {
return this.map(
(t) => {
@ -146,7 +146,8 @@ export abstract class Store<T> implements Readable<T> {
return f(<Exclude<T, undefined | null>>t)
},
typeof extraStoresToWatch === "function" ? [] : extraStoresToWatch,
callbackDestroyFunction ?? (typeof extraStoresToWatch === "function" ? extraStoresToWatch : undefined),
callbackDestroyFunction ??
(typeof extraStoresToWatch === "function" ? extraStoresToWatch : undefined)
)
}
@ -219,9 +220,16 @@ export abstract class Store<T> implements Readable<T> {
* src.setData(0)
* lastValue // => "def"
*/
public bind<X>(f: (t: T) => Store<X>, extraSources?: Store<unknown>[] | ((f: () => void) => void), onDestroy?: (f : () => void) => void): Store<X> {
const mapped = this.map(f, typeof extraSources === "function" ? undefined : extraSources,
onDestroy ?? (typeof extraSources === "function" ? extraSources : undefined))
public bind<X>(
f: (t: T) => Store<X>,
extraSources?: Store<unknown>[] | ((f: () => void) => void),
onDestroy?: (f: () => void) => void
): Store<X> {
const mapped = this.map(
f,
typeof extraSources === "function" ? undefined : extraSources,
onDestroy ?? (typeof extraSources === "function" ? extraSources : undefined)
)
const sink = new UIEventSource<X>(undefined)
const seenEventSources = new Set<Store<X>>()
mapped.addCallbackAndRun((newEventSource) => {
@ -255,17 +263,21 @@ export abstract class Store<T> implements Readable<T> {
public bindD<X>(
f: (t: Exclude<T, undefined | null>) => Store<X>,
extraSources?: Store<unknown>[],
onDestroy?: ((f: () => void) => void)
onDestroy?: (f: () => void) => void
): Store<X> {
return this.bind((t) => {
if (t === null) {
return null
}
if (t === undefined) {
return undefined
}
return f(<Exclude<T, undefined | null>>t)
}, extraSources, onDestroy)
return this.bind(
(t) => {
if (t === null) {
return null
}
if (t === undefined) {
return undefined
}
return f(<Exclude<T, undefined | null>>t)
},
extraSources,
onDestroy
)
}
public stabilized(millisToStabilize): Store<T> {
@ -337,6 +349,16 @@ export abstract class Store<T> implements Readable<T> {
}
})
}
/**
* Create a new UIEVentSource. Whenever 'this.data' changes, the returned UIEventSource will get this value as well.
* However, this value can be overridden without affecting source
*/
public followingClone(): UIEventSource<T> {
const src = new UIEventSource(this.data)
this.addCallback((t) => src.setData(t))
return src
}
}
export class ImmutableStore<T> extends Store<T> {
@ -349,8 +371,7 @@ export class ImmutableStore<T> extends Store<T> {
this.data = data
}
private static readonly pass: () => void = () => {
}
private static readonly pass: () => void = () => {}
addCallback(_: (data: T) => void): () => void {
// pass: data will never change
@ -378,8 +399,8 @@ export class ImmutableStore<T> extends Store<T> {
map<J>(
f: (t: T) => J,
extraStores: Store<any>[] | ((f: () => void) => void)= undefined,
ondestroyCallback?: (f: () => void) => void,
extraStores: Store<any>[] | ((f: () => void) => void) = undefined,
ondestroyCallback?: (f: () => void) => void
): ImmutableStore<J> {
if (extraStores?.length > 0) {
return new MappedStore(this, f, extraStores, undefined, f(this.data), ondestroyCallback)
@ -489,7 +510,7 @@ class MappedStore<TIn, T> extends Store<T> {
extraStores: Store<unknown>[] | ((t: () => void) => void),
upstreamListenerHandler: ListenerTracker<TIn> | undefined,
initialState: T,
onDestroy?: (f: () => void) => void,
onDestroy?: (f: () => void) => void
) {
super()
this._upstream = upstream
@ -533,11 +554,10 @@ class MappedStore<TIn, T> extends Store<T> {
map<J>(
f: (t: T) => J,
extraStores: Store<unknown>[] | ((f: () => void) => void) = undefined,
ondestroyCallback?: (f: () => void) => void,
ondestroyCallback?: (f: () => void) => void
): Store<J> {
let stores: Store<unknown>[] = undefined
if (typeof extraStores !== "function") {
if (extraStores?.length > 0 || this._extraStores?.length > 0) {
stores = []
}
@ -559,7 +579,7 @@ class MappedStore<TIn, T> extends Store<T> {
stores,
this._callbacks,
f(this.data),
ondestroyCallback,
ondestroyCallback
)
}
@ -608,7 +628,7 @@ class MappedStore<TIn, T> extends Store<T> {
if (!this._callbacksAreRegistered) {
return
}
this._upstreamPingCount = this._upstreamCallbackHandler.pingCount
this._upstreamPingCount = this._upstreamCallbackHandler?.pingCount
this._callbacksAreRegistered = false
this._unregisterFromUpstream()
this._unregisterFromExtraStores?.forEach((unr) => unr())
@ -617,7 +637,7 @@ class MappedStore<TIn, T> extends Store<T> {
private registerCallbacksToUpstream() {
this._unregisterFromUpstream = this._upstream.addCallback(() => this.update())
this._unregisterFromExtraStores = this._extraStores?.map((store) =>
store?.addCallback(() => this.update()),
store?.addCallback(() => this.update())
)
this._callbacksAreRegistered = true
}
@ -638,8 +658,7 @@ class MappedStore<TIn, T> extends Store<T> {
}
export class UIEventSource<T> extends Store<T> implements Writable<T> {
private static readonly pass: () => void = () => {
}
private static readonly pass: () => void = () => {}
public data: T
_callbacks: ListenerTracker<T> = new ListenerTracker<T>()
@ -654,7 +673,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public static flatten<X>(
source: Store<Store<X>>,
possibleSources?: Store<object>[],
possibleSources?: Store<object>[]
): UIEventSource<X> {
const sink = new UIEventSource<X>(source.data?.data)
@ -683,7 +702,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
*/
public static fromPromise<T>(
promise: Promise<T>,
onError: (e) => void = undefined,
onError: (e) => void = undefined
): UIEventSource<T> {
const src = new UIEventSource<T>(undefined)
promise?.then((d) => src.setData(d))
@ -704,7 +723,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
* @constructor
*/
public static fromPromiseWithErr<T>(
promise: Promise<T>,
promise: Promise<T>
): UIEventSource<{ success: T } | { error: any } | undefined> {
const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
promise
@ -737,7 +756,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return undefined
}
return "" + fl
},
}
)
}
@ -768,7 +787,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return undefined
}
return "" + fl
},
}
)
}
@ -776,13 +795,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return stringUIEventSource.sync(
(str) => str === "true",
[],
(b) => "" + b,
(b) => "" + b
)
}
static asObject<T extends object | string>(
stringUIEventSource: UIEventSource<string>,
defaultV: T,
defaultV: T
): UIEventSource<T> {
return stringUIEventSource.sync(
(str) => {
@ -798,26 +817,15 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
"due to",
e,
"; the underlying data store has tag",
stringUIEventSource.tag,
stringUIEventSource.tag
)
return defaultV
}
},
[],
(b) => JSON.stringify(b) ?? "",
(b) => JSON.stringify(b) ?? ""
)
}
/**
* 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
}
/**
* Adds a callback
*
@ -894,16 +902,13 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public map<J>(
f: (t: T) => J,
extraSources?: Store<unknown>[],
onDestroy?: (f: () => void) => void,
)
public map<J>(
f: (t: T) => J,
onDestroy: (f: () => void) => void,
onDestroy?: (f: () => void) => void
)
public map<J>(f: (t: T) => J, onDestroy: (f: () => void) => void)
public map<J>(
f: (t: T) => J,
extraSources: Store<unknown>[] | ((f: () => void) => void),
onDestroy?: (f: () => void) => void,
onDestroy?: (f: () => void) => void
): Store<J> {
return new MappedStore(this, f, extraSources, this._callbacks, f(this.data), onDestroy)
}
@ -915,7 +920,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
public mapD<J>(
f: (t: Exclude<T, undefined | null>) => J,
extraSources?: Store<unknown>[] | ((f: () => void) => void),
callbackDestroyFunction?: (f: () => void) => void,
callbackDestroyFunction?: (f: () => void) => void
): Store<J | undefined> {
return new MappedStore(
this,
@ -933,7 +938,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
this.data === undefined || this.data === null
? <undefined | null>this.data
: f(<Exclude<T, undefined | null>>this.data),
callbackDestroyFunction,
callbackDestroyFunction
)
}
@ -953,7 +958,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
f: (t: T) => J,
extraSources: Store<unknown>[],
g: (j: J, t: T) => T,
allowUnregister = false,
allowUnregister = false
): UIEventSource<J> {
const stack = new Error().stack.split("\n")
const callee = stack[1]
@ -1002,11 +1007,15 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
this.setData(f(this.data))
}
public static concat<T>(stores: ReadonlyArray<UIEventSource<T | undefined>>): UIEventSource<(T | undefined)[]> ;
public static concat<T>(stores: ReadonlyArray<UIEventSource<T>>): UIEventSource<T[]> ;
public static concat<T>(stores: ReadonlyArray<UIEventSource<T | undefined>>): UIEventSource<(T | undefined)[]> {
public static concat<T>(
stores: ReadonlyArray<UIEventSource<T | undefined>>
): UIEventSource<(T | undefined)[]>
public static concat<T>(stores: ReadonlyArray<UIEventSource<T>>): UIEventSource<T[]>
public static concat<T>(
stores: ReadonlyArray<UIEventSource<T | undefined>>
): UIEventSource<(T | undefined)[]> {
const newStore = <UIEventSource<T[]>>Stores.concat(stores)
newStore.addCallbackD(list => {
newStore.addCallbackD((list) => {
for (let i = 0; i < list.length; i++) {
stores[i]?.setData(list[i])
}

View file

@ -97,9 +97,9 @@ export class AndroidPolyfill {
/**
* Gets how much padding we should add on top and at the bottom; in pixels
*/
private static insets: { top: Store<number>, bottom: Store<number> } = undefined
private static insets: { top: Store<number>; bottom: Store<number> } = undefined
public static getInsetSizes(): Readonly<{ top: Store<number>, bottom: Store<number> }> {
public static getInsetSizes(): Readonly<{ top: Store<number>; bottom: Store<number> }> {
if (AndroidPolyfill.insets) {
return AndroidPolyfill.insets
}
@ -110,19 +110,19 @@ export class AndroidPolyfill {
AndroidPolyfill.insets = insets
console.log("Web: requesting inset sizes")
DatabridgePluginSingleton.request<{ top: number, bottom: number }>({
DatabridgePluginSingleton.request<{ top: number; bottom: number }>({
key: "insets",
}).then((result) => {
if(!result){
if (!result) {
return
}
let v = result.value
if(typeof v === "string"){
if (typeof v === "string") {
v = JSON.parse(v)
}
console.log("Got inset sizes:", result)
insets.bottom.set(v.bottom)
insets.top.set(v.top)
insets.bottom.set(v.bottom / window.devicePixelRatio)
insets.top.set(v.top / window.devicePixelRatio)
})
return insets

View file

@ -3,11 +3,10 @@ import { Utils } from "../../Utils"
export class IsOnline {
private static readonly _isOnline: UIEventSource<boolean> = new UIEventSource(
Utils.runningFromConsole || navigator.onLine)
Utils.runningFromConsole || navigator.onLine
)
static {
if (!Utils.runningFromConsole) {
window.addEventListener("online", () => {
IsOnline._isOnline.set(true)
})
@ -19,5 +18,4 @@ export class IsOnline {
}
public static readonly isOnline: Store<boolean> = IsOnline._isOnline
}

View file

@ -146,7 +146,6 @@ export class MangroveIdentity {
}
}
/**
* Tracks all reviews of a given feature, allows to create a new review (and inserts this into the list)
*
@ -164,8 +163,9 @@ export default class FeatureReviews implements ReviewCollection {
private readonly _reviews: UIEventSource<
(Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> })[]
> = new UIEventSource(undefined)
public readonly reviews: Store<(Review & { kid: string, signature: string, madeByLoggedInUser: Store<boolean> })[]> =
this._reviews
public readonly reviews: Store<
(Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> })[]
> = this._reviews
private readonly _lat: number
private readonly _lon: number
private readonly _uncertainty: number
@ -182,7 +182,7 @@ export default class FeatureReviews implements ReviewCollection {
options?: Readonly<{
nameKey?: "name" | string
fallbackName?: string
uncertaintyRadius?: number,
uncertaintyRadius?: number
}>,
testmode?: Store<boolean>,
reportError?: (msg: string, extra: string) => Promise<void>
@ -285,7 +285,6 @@ export default class FeatureReviews implements ReviewCollection {
})
}
/**
* Construct a featureReviewsFor or fetches it from the cache
*
@ -307,7 +306,7 @@ export default class FeatureReviews implements ReviewCollection {
tagsSource: UIEventSource<Record<string, string>>,
mangroveIdentity: MangroveIdentity,
options: { nameKey: string; fallbackName: string },
state?: SpecialVisualizationState & WithUserRelatedState,
state?: SpecialVisualizationState & WithUserRelatedState
): FeatureReviews {
const key =
feature.properties.id +
@ -382,8 +381,8 @@ export default class FeatureReviews implements ReviewCollection {
this._identity.addReview(reviewWithKid)
}
private initReviews(){
if(this._reviews.data === undefined){
private initReviews() {
if (this._reviews.data === undefined) {
this._reviews.set([])
}
}
@ -452,16 +451,13 @@ export default class FeatureReviews implements ReviewCollection {
}
}
public async deleteReview(review: Review & {signature: string}){
public async deleteReview(review: Review & { signature: string }) {
await MangroveReviews.deleteReview(await this._identity.getKeypair(), review)
this.removeReviewLocally(review)
}
public removeReviewLocally(review: Review): void {
this._reviews.set(
this._reviews.data?.filter(r => r !== review)
)
this._reviews.set(this._reviews.data?.filter((r) => r !== review))
}
/**

View file

@ -10,6 +10,7 @@ import { Point } from "geojson"
import { ImageData, Panoramax, PanoramaxXYZ } from "panoramax-js/dist"
import { Mapillary } from "../ImageProviders/Mapillary"
import { ServerSourceInfo } from "../../Models/SourceOverview"
import { Lists } from "../../Utils/Lists"
interface ImageFetcher {
/**
@ -464,6 +465,7 @@ export class CombinedFetcher {
state.ping()
this.fetchImage(source, lat, lon, state, sink)
}
return { images: sink, state }
return { images: sink.mapD((imgs) => Lists.dedupOnId(imgs, (i) => i["id"])), state }
}
}

View file

@ -4,13 +4,17 @@ import { AndroidPolyfill } from "./AndroidPolyfill"
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { Feature } from "geojson"
import { Store, UIEventSource } from "../UIEventSource"
import ThemeViewState from "../../Models/ThemeViewState"
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
import ThemeSource from "../FeatureSource/Sources/ThemeSource"
export default class ThemeViewStateHashActor {
private readonly _state: {
private readonly _state: Readonly<{
indexedFeatures: IndexedFeatureSource
selectedElement: UIEventSource<Feature>
guistate: MenuState
}
osmObjectDownloader: OsmObjectDownloader
}>
private isUpdatingHash = false
public static readonly documentation = [
@ -35,12 +39,15 @@ export default class ThemeViewStateHashActor {
* As such, we use a change in the hash to close the appropriate windows
*
*/
constructor(state: {
featureSwitches: { featureSwitchBackToThemeOverview: Store<boolean> }
indexedFeatures: IndexedFeatureSource
selectedElement: UIEventSource<Feature>
guistate: MenuState
}) {
constructor(
state: Readonly<{
featureSwitches: { featureSwitchBackToThemeOverview: Store<boolean> }
indexedFeatures: IndexedFeatureSource & ThemeSource
selectedElement: UIEventSource<Feature>
guistate: MenuState
osmObjectDownloader: OsmObjectDownloader
}>
) {
this._state = state
AndroidPolyfill.onBackButton(() => this.back(), {
returnToIndex: state.featureSwitches.featureSwitchBackToThemeOverview,
@ -50,6 +57,20 @@ export default class ThemeViewStateHashActor {
const containsMenu = this.loadStateFromHash(hashOnLoad)
// First of all, try to recover the selected element
if (!containsMenu && hashOnLoad?.length > 0) {
if (
hashOnLoad.startsWith("node/") ||
hashOnLoad.startsWith("way/") ||
hashOnLoad.startsWith("relation/")
) {
// This is an OSM-element. Let's download it and add it to the indexedFeatures
console.log("Directly downloading item from hash")
state.osmObjectDownloader.DownloadObjectAsync(hashOnLoad).then((osmObj) => {
if (osmObj === "deleted") {
return
}
state.indexedFeatures.addItem(osmObj.asGeoJson())
})
}
state.indexedFeatures.featuresById.addCallbackAndRunD(() => {
// once that we have found a matching element, we can be sure the indexedFeaturesource was popuplated and that the job is done
return this.loadSelectedElementFromHash(hashOnLoad)