forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
cac87a5467
727 changed files with 27522 additions and 15764 deletions
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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([])
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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("&")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[]> {
|
||||
|
|
|
|||
|
|
@ -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})`
|
||||
),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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})`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[]> {
|
||||
|
|
|
|||
|
|
@ -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> = {
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(/</g,'<')?.replace(/>/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(/</g, "<")?.replace(/>/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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>` +
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue