From e4eb8d6b527732ef7d0dc3280ef7fc2049c47fc6 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Wed, 21 Feb 2024 16:35:49 +0100 Subject: [PATCH] Download button: take advantage of MVT server, download button will now attempt to download everything --- langs/en.json | 1 + src/Logic/FeatureSource/FeatureSource.ts | 10 +- .../Sources/FeatureSourceMerger.ts | 33 +++- .../FeatureSource/Sources/GeoJsonSource.ts | 122 +++++++------- .../FeatureSource/Sources/LayoutSource.ts | 27 ++- src/Logic/FeatureSource/Sources/MvtSource.ts | 122 +++++++------- .../Sources/OverpassFeatureSource.ts | 19 +-- .../DynamicGeoJsonTileSource.ts | 6 +- .../DynamicMvtTileSource.ts | 158 +++++++++--------- .../TiledFeatureSource/DynamicTileSource.ts | 124 +++++++++----- .../TiledFeatureSource/LineSourceMerger.ts | 52 +++--- .../TiledFeatureSource/PolygonSourceMerger.ts | 13 +- .../TiledFeatureSource/SummaryTileSource.ts | 17 +- src/Models/ThemeViewState.ts | 32 ++-- src/UI/DownloadFlow/DownloadButton.svelte | 20 +-- src/UI/DownloadFlow/DownloadHelper.ts | 3 +- src/UI/DownloadFlow/DownloadPanel.svelte | 22 ++- src/UI/DownloadFlow/DownloadPdf.svelte | 7 +- src/UI/SpecialVisualization.ts | 5 +- src/UI/StatisticsGUI.ts | 1 - src/Utils/svgToPdf.ts | 12 +- 21 files changed, 453 insertions(+), 353 deletions(-) diff --git a/langs/en.json b/langs/en.json index 213f7fff7..7e7284eed 100644 --- a/langs/en.json +++ b/langs/en.json @@ -213,6 +213,7 @@ "current_view_generic": "Export a PDF off the current view for {paper_size} in {orientation} orientation" }, "title": "Download", + "toMuch": "There are to much features to download them all", "uploadGpx": "Upload your track to OpenStreetMap" }, "enableGeolocationForSafari": "Did you not get the popup to ask for geopermission?", diff --git a/src/Logic/FeatureSource/FeatureSource.ts b/src/Logic/FeatureSource/FeatureSource.ts index bd5ecbfc2..a0a32d29c 100644 --- a/src/Logic/FeatureSource/FeatureSource.ts +++ b/src/Logic/FeatureSource/FeatureSource.ts @@ -5,6 +5,13 @@ import { Feature } from "geojson" export interface FeatureSource { features: Store } + +export interface UpdatableFeatureSource extends FeatureSource { + /** + * Forces an update and downloads the data, even if the feature source is supposed to be active + */ + updateAsync() +} export interface WritableFeatureSource extends FeatureSource { features: UIEventSource } @@ -16,11 +23,10 @@ export interface FeatureSourceForLayer extends Feat readonly layer: FilteredLayer } -export interface FeatureSourceForTile extends FeatureSource { +export interface FeatureSourceForTile extends FeatureSource { readonly x: number readonly y: number readonly z: number - } /** * A feature source which is aware of the indexes it contains diff --git a/src/Logic/FeatureSource/Sources/FeatureSourceMerger.ts b/src/Logic/FeatureSource/Sources/FeatureSourceMerger.ts index 7ea4d1635..3ba2c785c 100644 --- a/src/Logic/FeatureSource/Sources/FeatureSourceMerger.ts +++ b/src/Logic/FeatureSource/Sources/FeatureSourceMerger.ts @@ -1,18 +1,20 @@ import { Store, UIEventSource } from "../../UIEventSource" -import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" +import { FeatureSource, IndexedFeatureSource, UpdatableFeatureSource } from "../FeatureSource" import { Feature } from "geojson" import { Utils } from "../../../Utils" -import DynamicTileSource from "../TiledFeatureSource/DynamicTileSource" /** * 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 implements IndexedFeatureSource { +export default class FeatureSourceMerger + implements IndexedFeatureSource +{ public features: UIEventSource = new UIEventSource([]) public readonly featuresById: Store> protected readonly _featuresById: UIEventSource> - private readonly _sources: Src[] = [] + protected readonly _sources: Src[] + /** * Merges features from different featureSources. * In case that multiple features have the same id, the latest `_version_number` will be used. Otherwise, we will take the last one @@ -27,22 +29,25 @@ export default class FeatureSourceMerger { this.addDataFromSources(this._sources) }) } - protected addDataFromSources(sources: Src[]){ - this.addData(sources.map(s => s.features.data)) + protected addDataFromSources(sources: Src[]) { + this.addData(sources.map((s) => s.features.data)) } protected addData(sources: Feature[][]) { @@ -93,3 +98,17 @@ export default class FeatureSourceMerger + extends FeatureSourceMerger + implements IndexedFeatureSource, UpdatableFeatureSource +{ + constructor(...sources: Src[]) { + super(...sources) + } + async updateAsync() { + await Promise.all(this._sources.map((src) => src.updateAsync())) + } +} diff --git a/src/Logic/FeatureSource/Sources/GeoJsonSource.ts b/src/Logic/FeatureSource/Sources/GeoJsonSource.ts index 22bd6979a..dd5abbbab 100644 --- a/src/Logic/FeatureSource/Sources/GeoJsonSource.ts +++ b/src/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -11,9 +11,14 @@ import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import { Tiles } from "../../../Models/TileRange" export default class GeoJsonSource implements FeatureSource { - public readonly features: Store + private readonly _features: UIEventSource = new UIEventSource(undefined) + public readonly features: Store = this._features private readonly seenids: Set private readonly idKey?: string + private readonly url: string + private readonly layer: LayerConfig + private _isDownloaded = false + private currentlyRunning: Promise public constructor( layer: LayerConfig, @@ -30,6 +35,7 @@ export default class GeoJsonSource implements FeatureSource { this.idKey = layer.source.idKey this.seenids = options?.featureIdBlacklist ?? new Set() let url = layer.source.geojsonSource.replace("{layer}", layer.id) + this.layer = layer let zxy = options?.zxy if (zxy !== undefined) { let tile_bbox: BBox @@ -57,86 +63,88 @@ export default class GeoJsonSource implements FeatureSource { .replace("{x_min}", "" + bounds.minLon) .replace("{x_max}", "" + bounds.maxLon) } + this.url = url - const eventsource = new UIEventSource([]) if (options?.isActive !== undefined) { options.isActive.addCallbackAndRunD(async (active) => { if (!active) { return } - this.LoadJSONFrom(url, eventsource, layer) - .then((fs) => console.debug("Loaded", fs.length, "features from", url)) - .catch((err) => console.warn("Could not load ", url, "due to", err)) + this.updateAsync() return true // data is loaded, we can safely unregister }) } else { - this.LoadJSONFrom(url, eventsource, layer) - .then((fs) => console.debug("Loaded", fs.length, "features from", url)) - .catch((err) => console.warn("Could not load ", url, "due to", err)) + this.updateAsync() } - this.features = eventsource + } + + public async updateAsync(): Promise { + if (!this.currentlyRunning) { + this.currentlyRunning = this.LoadJSONFrom() + } + await this.currentlyRunning } /** * Init the download, write into the specified event source for the given layer. * Note this method caches the requested geojson for five minutes */ - private async LoadJSONFrom( - url: string, - eventSource: UIEventSource, - layer: LayerConfig, - options?: { - maxCacheAgeSec?: number | 300 + private async LoadJSONFrom(options?: { maxCacheAgeSec?: number | 300 }): Promise { + if (this._isDownloaded) { + return } - ): Promise { - const self = this - let json = await Utils.downloadJsonCached(url, (options?.maxCacheAgeSec ?? 300) * 1000) + const url = this.url + try { + let json = await Utils.downloadJsonCached(url, (options?.maxCacheAgeSec ?? 300) * 1000) - if (json.features === undefined || json.features === null) { - json.features = [] - } - - if (layer.source.mercatorCrs) { - json = GeoOperations.GeoJsonToWGS84(json) - } - - const time = new Date() - const newFeatures: Feature[] = [] - let i = 0 - for (const feature of json.features) { - if (feature.geometry.type === "Point") { - // See https://github.com/maproulette/maproulette-backend/issues/242 - feature.geometry.coordinates = feature.geometry.coordinates.map(Number) + if (json.features === undefined || json.features === null) { + json.features = [] } - const props = feature.properties - for (const key in props) { - if (props[key] === null) { - delete props[key] + + if (this.layer.source.mercatorCrs) { + json = GeoOperations.GeoJsonToWGS84(json) + } + + const newFeatures: Feature[] = [] + let i = 0 + for (const feature of json.features) { + if (feature.geometry.type === "Point") { + // See https://github.com/maproulette/maproulette-backend/issues/242 + feature.geometry.coordinates = feature.geometry.coordinates.map(Number) + } + const props = feature.properties + for (const key in props) { + if (props[key] === null) { + delete props[key] + } + + if (typeof props[key] !== "string") { + // Make sure all the values are string, it crashes stuff otherwise + props[key] = JSON.stringify(props[key]) + } } - if (typeof props[key] !== "string") { - // Make sure all the values are string, it crashes stuff otherwise - props[key] = JSON.stringify(props[key]) + if (this.idKey !== undefined) { + props.id = props[this.idKey] } + + if (props.id === undefined) { + props.id = url + "/" + i + feature.id = url + "/" + i + i++ + } + if (this.seenids.has(props.id)) { + continue + } + this.seenids.add(props.id) + newFeatures.push(feature) } - if (self.idKey !== undefined) { - props.id = props[self.idKey] - } - - if (props.id === undefined) { - props.id = url + "/" + i - feature.id = url + "/" + i - i++ - } - if (self.seenids.has(props.id)) { - continue - } - self.seenids.add(props.id) - newFeatures.push(feature) + this._features.setData(newFeatures) + this._isDownloaded = true + return newFeatures + } catch (e) { + console.warn("Could not load ", url, "due to", e) } - - eventSource.setData(newFeatures) - return newFeatures } } diff --git a/src/Logic/FeatureSource/Sources/LayoutSource.ts b/src/Logic/FeatureSource/Sources/LayoutSource.ts index ff5b5fda8..60f24ab64 100644 --- a/src/Logic/FeatureSource/Sources/LayoutSource.ts +++ b/src/Logic/FeatureSource/Sources/LayoutSource.ts @@ -1,17 +1,17 @@ import GeoJsonSource from "./GeoJsonSource" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" -import { FeatureSource } from "../FeatureSource" +import { UpdatableFeatureSource } from "../FeatureSource" import { Or } from "../../Tags/Or" import FeatureSwitchState from "../../State/FeatureSwitchState" import OverpassFeatureSource from "./OverpassFeatureSource" import { Store, UIEventSource } from "../../UIEventSource" import OsmFeatureSource from "./OsmFeatureSource" -import FeatureSourceMerger from "./FeatureSourceMerger" import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" import { BBox } from "../../BBox" import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource" import DynamicMvtileSource from "../TiledFeatureSource/DynamicMvtTileSource" +import FeatureSourceMerger from "./FeatureSourceMerger" /** * This source will fetch the needed data from various sources for the given layout. @@ -24,6 +24,8 @@ export default class LayoutSource extends FeatureSourceMerger { */ public readonly isLoading: Store + private readonly supportsForceDownload: UpdatableFeatureSource[] + constructor( layers: LayerConfig[], featureSwitches: FeatureSwitchState, @@ -33,6 +35,8 @@ export default class LayoutSource extends FeatureSourceMerger { mvtAvailableLayers: Set, fullNodeDatabaseSource?: FullNodeDatabaseSource ) { + const supportsForceDownload: UpdatableFeatureSource[] = [] + const { bounds, zoom } = mapProperties // remove all 'special' layers layers = layers.filter((layer) => layer.source !== null && layer.source !== undefined) @@ -46,7 +50,7 @@ export default class LayoutSource extends FeatureSourceMerger { maxAge: l.maxAgeOfCache, }) ) - const mvtSources: FeatureSource[] = osmLayers + const mvtSources: UpdatableFeatureSource[] = osmLayers .filter((f) => mvtAvailableLayers.has(f.id)) .map((l) => LayoutSource.setupMvtSource(l, mapProperties, isDisplayed(l.id))) const nonMvtSources = [] @@ -79,24 +83,29 @@ export default class LayoutSource extends FeatureSourceMerger { const loading = overpassSource?.runningQuery?.data || osmApiSource?.isRunning?.data isLoading.setData(loading) } + overpassSource?.runningQuery?.addCallbackAndRun((_) => setIsLoading()) osmApiSource?.isRunning?.addCallbackAndRun((_) => setIsLoading()) + supportsForceDownload.push(overpassSource) } - const geojsonSources: FeatureSource[] = geojsonlayers.map((l) => + const geojsonSources: UpdatableFeatureSource[] = geojsonlayers.map((l) => LayoutSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id)) ) super(...geojsonSources, ...fromCache, ...mvtSources, ...nonMvtSources) this.isLoading = isLoading + supportsForceDownload.push(...geojsonSources) + supportsForceDownload.push(...mvtSources) // Non-mvt sources are handled by overpass + this.supportsForceDownload = supportsForceDownload } private static setupMvtSource( layer: LayerConfig, mapProperties: { zoom: Store; bounds: Store }, isActive?: Store - ): FeatureSource { + ): UpdatableFeatureSource { return new DynamicMvtileSource(layer, mapProperties, { isActive }) } @@ -104,7 +113,7 @@ export default class LayoutSource extends FeatureSourceMerger { layer: LayerConfig, mapProperties: { zoom: Store; bounds: Store }, isActive?: Store - ): FeatureSource { + ): UpdatableFeatureSource { const source = layer.source isActive = mapProperties.zoom.map( (z) => (isActive?.data ?? true) && z >= layer.minzoom, @@ -190,4 +199,10 @@ export default class LayoutSource extends FeatureSourceMerger { } ) } + + public async downloadAll() { + console.log("Downloading all data") + await Promise.all(this.supportsForceDownload.map((i) => i.updateAsync())) + console.log("Done") + } } diff --git a/src/Logic/FeatureSource/Sources/MvtSource.ts b/src/Logic/FeatureSource/Sources/MvtSource.ts index 4ffcc45cc..ed66dcd51 100644 --- a/src/Logic/FeatureSource/Sources/MvtSource.ts +++ b/src/Logic/FeatureSource/Sources/MvtSource.ts @@ -1,8 +1,7 @@ -import { Geometry } from "geojson" -import { Feature as GeojsonFeature } from "geojson" +import { Feature as GeojsonFeature, Geometry } from "geojson" import { Store, UIEventSource } from "../../UIEventSource" -import { FeatureSourceForTile } from "../FeatureSource" +import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" import Pbf from "pbf" type Coords = [number, number][] @@ -205,6 +204,7 @@ class Layer { end ) } + static _readField(tag, obj, pbf) { if (tag === 15) obj.version = pbf.readVarint() else if (tag === 1) obj.name = pbf.readString() @@ -213,6 +213,7 @@ class Layer { else if (tag === 4) obj.values.push(Value.read(pbf, pbf.readVarint() + pbf.pos)) else if (tag === 5) obj.extent = pbf.readVarint() } + public static write(obj, pbf) { if (obj.version) pbf.writeVarintField(15, obj.version) if (obj.name) pbf.writeStringField(1, obj.name) @@ -230,12 +231,14 @@ class Feature { static read(pbf, end) { return pbf.readFields(Feature._readField, { id: 0, tags: [], type: 0, geometry: [] }, end) } + static _readField(tag, obj, pbf) { if (tag === 1) obj.id = pbf.readVarint() else if (tag === 2) pbf.readPackedVarint(obj.tags) else if (tag === 3) obj.type = pbf.readVarint() else if (tag === 4) pbf.readPackedVarint(obj.geometry) } + public static write(obj, pbf) { if (obj.id) pbf.writeVarintField(1, obj.id) if (obj.tags) pbf.writePackedVarint(2, obj.tags) @@ -260,6 +263,7 @@ class Value { end ) } + static _readField = function (tag, obj, pbf) { if (tag === 1) obj.string_value = pbf.readString() else if (tag === 2) obj.float_value = pbf.readFloat() @@ -269,6 +273,7 @@ class Value { else if (tag === 6) obj.sint_value = pbf.readSVarint() else if (tag === 7) obj.bool_value = pbf.readBoolean() } + public static write(obj, pbf) { if (obj.string_value) pbf.writeStringField(1, obj.string_value) if (obj.float_value) pbf.writeFloatField(2, obj.float_value) @@ -279,21 +284,10 @@ class Value { if (obj.bool_value) pbf.writeBooleanField(7, obj.bool_value) } } + class Tile { // code generated by pbf v3.2.1 - public static read(pbf, end) { - return pbf.readFields(Tile._readField, { layers: [] }, end) - } - static _readField(tag, obj, pbf) { - if (tag === 3) obj.layers.push(Layer.read(pbf, pbf.readVarint() + pbf.pos)) - } - static write(obj, pbf) { - if (obj.layers) - for (var i = 0; i < obj.layers.length; i++) - pbf.writeMessage(3, Layer.write, obj.layers[i]) - } - static GeomType = { UNKNOWN: { value: 0, @@ -312,10 +306,27 @@ class Tile { options: {}, }, } + + public static read(pbf, end) { + return pbf.readFields(Tile._readField, { layers: [] }, end) + } + + static _readField(tag, obj, pbf) { + if (tag === 3) obj.layers.push(Layer.read(pbf, pbf.readVarint() + pbf.pos)) + } + + static write(obj, pbf) { + if (obj.layers) + for (var i = 0; i < obj.layers.length; i++) + pbf.writeMessage(3, Layer.write, obj.layers[i]) + } } -export default class MvtSource implements FeatureSourceForTile { +export default class MvtSource implements FeatureSourceForTile, UpdatableFeatureSource { public readonly features: Store[]> + public readonly x: number + public readonly y: number + public readonly z: number private readonly _url: string private readonly _layerName: string private readonly _features: UIEventSource< @@ -326,9 +337,7 @@ export default class MvtSource implements FeatureSourceForTile { } >[] > = new UIEventSource[]>([]) - public readonly x: number - public readonly y: number - public readonly z: number + private currentlyRunning: Promise constructor( url: string, @@ -343,7 +352,7 @@ export default class MvtSource implements FeatureSourceForTile { this.x = x this.y = y this.z = z - this.downloadSync() + this.updateAsync() this.features = this._features.map( (fs) => { if (fs === undefined || isActive?.data === false) { @@ -355,6 +364,13 @@ export default class MvtSource implements FeatureSourceForTile { ) } + async updateAsync() { + if (!this.currentlyRunning) { + this.currentlyRunning = this.download() + } + await this.currentlyRunning + } + private getValue(v: { // Exactly one of these values must be present in a valid message string_value?: string @@ -389,47 +405,37 @@ export default class MvtSource implements FeatureSourceForTile { return undefined } - private downloadSync() { - this.download() - .then((d) => { - if (d.length === 0) { - return - } - return this._features.setData(d) - }) - .catch((e) => { - console.error(e) - }) - } - - private async download(): Promise { - const result = await fetch(this._url) - if (result.status !== 200) { - console.error("Could not download tile " + this._url) - return [] - } - const buffer = await result.arrayBuffer() - const data = Tile.read(new Pbf(buffer), undefined) - const layers = data.layers - let layer = data.layers[0] - if (layers.length > 1) { - if (!this._layerName) { - throw "Multiple layers in the downloaded tile, but no layername is given to choose from" + private async download(): Promise { + try { + const result = await fetch(this._url) + if (result.status !== 200) { + console.error("Could not download tile " + this._url) + return } - layer = layers.find((l) => l.name === this._layerName) - } - if (!layer) { - return [] - } - const builder = new MvtFeatureBuilder(layer.extent, this.x, this.y, this.z) - const features: GeojsonFeature[] = [] + const buffer = await result.arrayBuffer() + const data = Tile.read(new Pbf(buffer), undefined) + const layers = data.layers + let layer = data.layers[0] + if (layers.length > 1) { + if (!this._layerName) { + throw "Multiple layers in the downloaded tile, but no layername is given to choose from" + } + layer = layers.find((l) => l.name === this._layerName) + } + if (!layer) { + return + } + const builder = new MvtFeatureBuilder(layer.extent, this.x, this.y, this.z) + const features: GeojsonFeature[] = [] - for (const feature of layer.features) { - const properties = this.inflateProperties(feature.tags, layer.keys, layer.values) - features.push(builder.toGeoJson(feature.geometry, feature.type, properties)) + for (const feature of layer.features) { + const properties = this.inflateProperties(feature.tags, layer.keys, layer.values) + features.push(builder.toGeoJson(feature.geometry, feature.type, properties)) + } + this._features.setData(features) + } catch (e) { + console.error("Could not download MVT tile due to", e) } - - return features } private inflateProperties(tags: number[], keys: string[], values: { string_value: string }[]) { diff --git a/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts b/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts index 02ef73c43..9ff8f74cf 100644 --- a/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/OverpassFeatureSource.ts @@ -1,5 +1,5 @@ import { Feature } from "geojson" -import { FeatureSource } from "../FeatureSource" +import { FeatureSource, UpdatableFeatureSource } from "../FeatureSource" import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import { Or } from "../../Tags/Or" @@ -12,7 +12,7 @@ import { BBox } from "../../BBox" * 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 FeatureSource { +export default class OverpassFeatureSource implements UpdatableFeatureSource { /** * The last loaded features, as geojson */ @@ -99,21 +99,15 @@ export default class OverpassFeatureSource implements FeatureSource { ) { return undefined } - const result = await this.updateAsync() - if (!result) { - return - } - const [bounds, _, __] = result - this._lastQueryBBox = bounds + await this.updateAsync() } /** * Download the relevant data from overpass. Attempt to use a different server; only downloads the relevant layers * @private */ - private async updateAsync(): Promise<[BBox, Date, LayerConfig[]]> { + public async updateAsync(): Promise { let data: any = undefined - let date: Date = undefined let lastUsed = 0 const layersToDownload = [] @@ -172,7 +166,7 @@ export default class OverpassFeatureSource implements FeatureSource { return undefined } this.runningQuery.setData(true) - ;[data, date] = await overpass.queryGeoJson(bounds) + data = await overpass.queryGeoJson(bounds)[0] } catch (e) { self.retries.data++ self.retries.ping() @@ -205,10 +199,9 @@ export default class OverpassFeatureSource implements FeatureSource { console.log("Overpass returned", data.features.length, "features") self.features.setData(data.features) - return [bounds, date, layersToDownload] + this._lastQueryBBox = bounds } catch (e) { console.error("Got the overpass response, but could not process it: ", e, e.stack) - return undefined } finally { self.retries.setData(0) self.runningQuery.setData(false) diff --git a/src/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts index 010af52ff..7cb342856 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource.ts @@ -1,11 +1,11 @@ import { ImmutableStore, Store } from "../../UIEventSource" -import DynamicTileSource from "./DynamicTileSource" +import { UpdatableDynamicTileSource } from "./DynamicTileSource" import { Utils } from "../../../Utils" import GeoJsonSource from "../Sources/GeoJsonSource" import { BBox } from "../../BBox" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" -export default class DynamicGeoJsonTileSource extends DynamicTileSource { +export default class DynamicGeoJsonTileSource extends UpdatableDynamicTileSource { private static whitelistCache = new Map() constructor( @@ -65,7 +65,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { const blackList = new Set() super( - new ImmutableStore(source.geojsonZoomLevel), + new ImmutableStore(source.geojsonZoomLevel), layer.minzoom, (zxy) => { if (whitelist !== undefined) { diff --git a/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts index ed379bfb1..22b18b258 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts @@ -1,78 +1,16 @@ import { Store } from "../../UIEventSource" -import DynamicTileSource from "./DynamicTileSource" +import { UpdatableDynamicTileSource } from "./DynamicTileSource" import { Utils } from "../../../Utils" import { BBox } from "../../BBox" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" import MvtSource from "../Sources/MvtSource" import { Tiles } from "../../../Models/TileRange" import Constants from "../../../Models/Constants" -import FeatureSourceMerger from "../Sources/FeatureSourceMerger" +import { UpdatableFeatureSourceMerger } from "../Sources/FeatureSourceMerger" import { LineSourceMerger } from "./LineSourceMerger" import { PolygonSourceMerger } from "./PolygonSourceMerger" - -class PolygonMvtSource extends PolygonSourceMerger{ - constructor( layer: LayerConfig, - mapProperties: { - zoom: Store - bounds: Store - }, - options?: { - isActive?: Store - }) { - const roundedZoom = mapProperties.zoom.mapD(z => Math.min(Math.floor(z/2)*2, 14)) - super( - roundedZoom, - layer.minzoom, - (zxy) => { - const [z, x, y] = Tiles.tile_from_index(zxy) - const url = Utils.SubstituteKeys(Constants.VectorTileServer, - { - z, x, y, layer: layer.id, - type: "polygons", - }) - return new MvtSource(url, x, y, z) - }, - mapProperties, - { - isActive: options?.isActive, - }) - } -} - - -class LineMvtSource extends LineSourceMerger{ - constructor( layer: LayerConfig, - mapProperties: { - zoom: Store - bounds: Store - }, - options?: { - isActive?: Store - }) { - const roundedZoom = mapProperties.zoom.mapD(z => Math.min(Math.floor(z/2)*2, 14)) - super( - roundedZoom, - layer.minzoom, - (zxy) => { - const [z, x, y] = Tiles.tile_from_index(zxy) - const url = Utils.SubstituteKeys(Constants.VectorTileServer, - { - z, x, y, layer: layer.id, - type: "lines", - }) - return new MvtSource(url, x, y, z) - }, - mapProperties, - { - isActive: options?.isActive, - }) - } -} - - -class PointMvtSource extends DynamicTileSource { - +class PolygonMvtSource extends PolygonSourceMerger { constructor( layer: LayerConfig, mapProperties: { @@ -81,31 +19,32 @@ class PointMvtSource extends DynamicTileSource { }, options?: { isActive?: Store - }, + } ) { - const roundedZoom = mapProperties.zoom.mapD(z => Math.min(Math.floor(z/2)*2, 14)) + const roundedZoom = mapProperties.zoom.mapD((z) => Math.min(Math.floor(z / 2) * 2, 14)) super( roundedZoom, layer.minzoom, (zxy) => { const [z, x, y] = Tiles.tile_from_index(zxy) - const url = Utils.SubstituteKeys(Constants.VectorTileServer, - { - z, x, y, layer: layer.id, - type: "pois", - }) + const url = Utils.SubstituteKeys(Constants.VectorTileServer, { + z, + x, + y, + layer: layer.id, + type: "polygons", + }) return new MvtSource(url, x, y, z) }, mapProperties, { isActive: options?.isActive, - }, + } ) } } -export default class DynamicMvtileSource extends FeatureSourceMerger { - +class LineMvtSource extends LineSourceMerger { constructor( layer: LayerConfig, mapProperties: { @@ -114,13 +53,80 @@ export default class DynamicMvtileSource extends FeatureSourceMerger { }, options?: { isActive?: Store + } + ) { + const roundedZoom = mapProperties.zoom.mapD((z) => Math.min(Math.floor(z / 2) * 2, 14)) + super( + roundedZoom, + layer.minzoom, + (zxy) => { + const [z, x, y] = Tiles.tile_from_index(zxy) + const url = Utils.SubstituteKeys(Constants.VectorTileServer, { + z, + x, + y, + layer: layer.id, + type: "lines", + }) + return new MvtSource(url, x, y, z) + }, + mapProperties, + { + isActive: options?.isActive, + } + ) + } +} + +class PointMvtSource extends UpdatableDynamicTileSource { + constructor( + layer: LayerConfig, + mapProperties: { + zoom: Store + bounds: Store }, + options?: { + isActive?: Store + } + ) { + const roundedZoom = mapProperties.zoom.mapD((z) => Math.min(Math.floor(z / 2) * 2, 14)) + super( + roundedZoom, + layer.minzoom, + (zxy) => { + const [z, x, y] = Tiles.tile_from_index(zxy) + const url = Utils.SubstituteKeys(Constants.VectorTileServer, { + z, + x, + y, + layer: layer.id, + type: "pois", + }) + return new MvtSource(url, x, y, z) + }, + mapProperties, + { + isActive: options?.isActive, + } + ) + } +} + +export default class DynamicMvtileSource extends UpdatableFeatureSourceMerger { + constructor( + layer: LayerConfig, + mapProperties: { + zoom: Store + bounds: Store + }, + options?: { + isActive?: Store + } ) { super( new PointMvtSource(layer, mapProperties, options), new LineMvtSource(layer, mapProperties, options), new PolygonMvtSource(layer, mapProperties, options) - ) } } diff --git a/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts index 4a429086c..ea8040832 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts @@ -1,7 +1,7 @@ import { Store, Stores } from "../../UIEventSource" import { Tiles } from "../../../Models/TileRange" import { BBox } from "../../BBox" -import { FeatureSource } from "../FeatureSource" +import { FeatureSource, UpdatableFeatureSource } from "../FeatureSource" import FeatureSourceMerger from "../Sources/FeatureSourceMerger" /*** @@ -11,6 +11,12 @@ import FeatureSourceMerger from "../Sources/FeatureSourceMerger" export default class DynamicTileSource< Src extends FeatureSource = FeatureSource > extends FeatureSourceMerger { + private readonly loadedTiles = new Set() + private readonly zDiff: number + private readonly zoomlevel: Store + private readonly constructSource: (tileIndex: number) => Src + private readonly bounds: Store + /** * * @param zoomlevel If {z} is specified in the source, the 'zoomlevel' will be used as zoomlevel to download from @@ -33,52 +39,86 @@ export default class DynamicTileSource< } ) { super() - const loadedTiles = new Set() - const zDiff = options?.zDiff ?? 0 + this.constructSource = constructSource + this.zoomlevel = zoomlevel + this.zDiff = options?.zDiff ?? 0 + this.bounds = mapProperties.bounds + const neededTiles: Store = Stores.ListStabilized( mapProperties.bounds - .mapD( - (bounds) => { - if (options?.isActive && !options?.isActive.data) { - return undefined - } + .mapD(() => { + if (options?.isActive && !options?.isActive.data) { + return undefined + } - if (mapProperties.zoom.data < minzoom) { - return undefined - } - const z = Math.floor(zoomlevel.data) + zDiff - const tileRange = Tiles.TileRangeBetween( - z, - bounds.getNorth(), - bounds.getEast(), - bounds.getSouth(), - bounds.getWest() - ) - if (tileRange.total > 500) { - console.warn( - "Got a really big tilerange, bounds and location might be out of sync" - ) - return undefined - } - - const needed = Tiles.MapRange(tileRange, (x, y) => - Tiles.tile_index(z, x, y) - ).filter((i) => !loadedTiles.has(i)) - if (needed.length === 0) { - return undefined - } - return needed - }, - [options?.isActive, mapProperties.zoom] - ) + if (mapProperties.zoom.data < minzoom) { + return undefined + } + return this.getNeededTileIndices() + }, [options?.isActive, mapProperties.zoom]) .stabilized(250) ) - neededTiles.addCallbackAndRunD((neededIndexes) => { - for (const neededIndex of neededIndexes) { - loadedTiles.add(neededIndex) - super.addSource(constructSource(neededIndex)) - } - }) + neededTiles.addCallbackAndRunD((neededIndexes) => this.downloadTiles(neededIndexes)) + } + + protected downloadTiles(neededIndexes: number[]): Src[] { + const sources: Src[] = [] + for (const neededIndex of neededIndexes) { + this.loadedTiles.add(neededIndex) + const src = this.constructSource(neededIndex) + super.addSource(src) + sources.push(src) + } + return sources + } + + protected getNeededTileIndices() { + const bounds = this.bounds.data + const z = Math.floor(this.zoomlevel.data) + this.zDiff + const tileRange = Tiles.TileRangeBetween( + z, + bounds.getNorth(), + bounds.getEast(), + bounds.getSouth(), + bounds.getWest() + ) + if (tileRange.total > 500) { + console.warn("Got a really big tilerange, bounds and location might be out of sync") + return [] + } + const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(z, x, y)).filter( + (i) => !this.loadedTiles.has(i) + ) + if (needed.length === 0) { + return [] + } + return needed + } +} + +export class UpdatableDynamicTileSource + extends DynamicTileSource + implements UpdatableFeatureSource +{ + constructor( + zoomlevel: Store, + minzoom: number, + constructSource: (tileIndex: number) => Src, + mapProperties: { + bounds: Store + zoom: Store + }, + options?: { + isActive?: Store + zDiff?: number + } + ) { + super(zoomlevel, minzoom, constructSource, mapProperties, options) + } + + async updateAsync() { + const sources = super.downloadTiles(super.getNeededTileIndices()) + await Promise.all(sources.map((src) => src.updateAsync())) } } diff --git a/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts b/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts index 8a1cb3909..03e26ac3f 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts @@ -1,30 +1,31 @@ -import { FeatureSourceForTile } from "../FeatureSource" +import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" import { Store } from "../../UIEventSource" import { BBox } from "../../BBox" import { Utils } from "../../../Utils" -import { Feature, LineString, MultiLineString, Position } from "geojson" -import { Tiles } from "../../../Models/TileRange" +import { Feature, MultiLineString, Position } from "geojson" import { GeoOperations } from "../../GeoOperations" -import DynamicTileSource from "./DynamicTileSource" +import { UpdatableDynamicTileSource } from "./DynamicTileSource" /** * 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 DynamicTileSource { +export class LineSourceMerger extends UpdatableDynamicTileSource< + FeatureSourceForTile & UpdatableFeatureSource +> { private readonly _zoomlevel: Store constructor( zoomlevel: Store, minzoom: number, - constructSource: (tileIndex: number) => FeatureSourceForTile, + constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource, mapProperties: { bounds: Store zoom: Store }, options?: { isActive?: Store - }, + } ) { super(zoomlevel, minzoom, constructSource, mapProperties, options) this._zoomlevel = zoomlevel @@ -35,33 +36,30 @@ export class LineSourceMerger extends DynamicTileSource { const all: Map> = new Map() const currentZoom = this._zoomlevel?.data ?? 0 for (const source of sources) { - if(source.z != currentZoom){ + if (source.z != currentZoom) { continue } - const bboxCoors = Tiles.tile_bounds_lon_lat(source.z, source.x, source.y) - const bboxGeo = new BBox(bboxCoors).asGeoJson({}) for (const f of source.features.data) { const id = f.properties.id - const coordinates : Position[][] = [] - if(f.geometry.type === "LineString"){ + const coordinates: Position[][] = [] + if (f.geometry.type === "LineString") { coordinates.push(f.geometry.coordinates) - }else if(f.geometry.type === "MultiLineString"){ + } else if (f.geometry.type === "MultiLineString") { coordinates.push(...f.geometry.coordinates) - }else { + } else { console.error("Invalid geometry type:", f.geometry.type) continue } const oldV = all.get(id) - if(!oldV){ - - all.set(id, { - type: "Feature", - properties: f.properties, - geometry:{ - type:"MultiLineString", - coordinates - } - }) + if (!oldV) { + all.set(id, { + type: "Feature", + properties: f.properties, + geometry: { + type: "MultiLineString", + coordinates, + }, + }) continue } oldV.geometry.coordinates.push(...coordinates) @@ -70,11 +68,13 @@ export class LineSourceMerger extends DynamicTileSource { const keys = Array.from(all.keys()) for (const key of keys) { - all.set(key, GeoOperations.attemptLinearize(>all.get(key))) + all.set( + key, + GeoOperations.attemptLinearize(>all.get(key)) + ) } const newList = Array.from(all.values()) this.features.setData(newList) this._featuresById.setData(all) } - } diff --git a/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts b/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts index 47327d872..716db8e27 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts @@ -1,27 +1,29 @@ -import { FeatureSourceForTile } from "../FeatureSource" +import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" import { Store } from "../../UIEventSource" import { BBox } from "../../BBox" import { Utils } from "../../../Utils" import { Feature } from "geojson" import { GeoOperations } from "../../GeoOperations" -import DynamicTileSource from "./DynamicTileSource" +import DynamicTileSource, { UpdatableDynamicTileSource } from "./DynamicTileSource" /** * 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 DynamicTileSource { +export class PolygonSourceMerger extends UpdatableDynamicTileSource< + FeatureSourceForTile & UpdatableFeatureSource +> { constructor( zoomlevel: Store, minzoom: number, - constructSource: (tileIndex: number) => FeatureSourceForTile, + constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource, mapProperties: { bounds: Store zoom: Store }, options?: { isActive?: Store - }, + } ) { super(zoomlevel, minzoom, constructSource, mapProperties, options) } @@ -69,5 +71,4 @@ export class PolygonSourceMerger extends DynamicTileSource this.features.setData(newList) this._featuresById.setData(all) } - } diff --git a/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts index 96370e296..81faaa289 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts @@ -14,6 +14,10 @@ export class SummaryTileSourceRewriter implements FeatureSource { private filteredLayers: FilteredLayer[] public readonly features: Store = this._features private readonly _summarySource: SummaryTileSource + private readonly _totalNumberOfFeatures: UIEventSource = new UIEventSource( + undefined + ) + public readonly totalNumberOfFeatures: Store = this._totalNumberOfFeatures constructor( summarySource: SummaryTileSource, filteredLayers: ReadonlyMap @@ -31,6 +35,7 @@ export class SummaryTileSourceRewriter implements FeatureSource { } private update() { + let fullTotal = 0 const newFeatures: Feature[] = [] const layersToCount = this.filteredLayers.filter((fl) => fl.isDisplayed.data) const bitmap = layersToCount.map((l) => (l.isDisplayed.data ? "1" : "0")).join("") @@ -42,10 +47,17 @@ export class SummaryTileSourceRewriter implements FeatureSource { } newFeatures.push({ ...f, - properties: { ...f.properties, id: f.properties.id + bitmap, total: newTotal }, + properties: { + ...f.properties, + id: f.properties.id + bitmap, + total: newTotal, + total_metric: Utils.numberWithMetrixPrefix(newTotal), + }, }) + fullTotal += newTotal } this._features.setData(newFeatures) + this._totalNumberOfFeatures.setData(fullTotal) } } @@ -94,7 +106,7 @@ export class SummaryTileSource extends DynamicTileSource { } const lat = counts["lat"] const lon = counts["lon"] - const total = Utils.numberWithMetrixPrefix(Number(counts["total"])) + const total = Number(counts["total"]) const tileBbox = new BBox(Tiles.tile_bounds_lon_lat(z, x, y)) if (!tileBbox.contains([lon, lat])) { console.error( @@ -116,6 +128,7 @@ export class SummaryTileSource extends DynamicTileSource { summary: "yes", ...counts, total, + total_metric: Utils.numberWithMetrixPrefix(total), layers: layersSummed, }, geometry: { diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index a5d4fb06a..fcc2b4436 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -114,6 +114,7 @@ export default class ThemeViewState implements SpecialVisualizationState { readonly closestFeatures: NearbyFeatureSource readonly newFeatures: WritableFeatureSource readonly layerState: LayerState + readonly featureSummary: SummaryTileSourceRewriter readonly perLayer: ReadonlyMap readonly perLayerFiltered: ReadonlyMap @@ -378,6 +379,7 @@ export default class ThemeViewState implements SpecialVisualizationState { ) this.favourites = new FavouritesFeatureSource(this) + this.featureSummary = this.setupSummaryLayer() this.initActors() this.drawSpecialLayers() this.initHotkeys() @@ -660,7 +662,17 @@ export default class ThemeViewState implements SpecialVisualizationState { ) } - private setupSummaryLayer(maxzoom: number) { + private setupSummaryLayer(): SummaryTileSourceRewriter { + /** + * MaxZoom for the summary layer + */ + const normalLayers = this.layout.layers.filter( + (l) => + Constants.priviliged_layers.indexOf(l.id) < 0 && + !l.id.startsWith("note_import") + ) + const maxzoom = Math.min(...normalLayers.map((l) => l.minzoom)) + const layers = this.layout.layers.filter( (l) => Constants.priviliged_layers.indexOf(l.id) < 0 && @@ -684,22 +696,6 @@ export default class ThemeViewState implements SpecialVisualizationState { private drawSpecialLayers() { type AddedByDefaultTypes = (typeof Constants.added_by_default)[number] const empty = [] - - /** - * MaxZoom for the summary layer - */ - const normalLayers = this.layout.layers.filter( - (l) => - Constants.priviliged_layers.indexOf(l.id) < 0 && - !l.id.startsWith("note_import") - ) - const maxzoom = Math.min(...normalLayers.map((l) => l.minzoom)) - console.log( - "Maxzoom for summary layer is", - maxzoom, - normalLayers.map((nl) => nl.id + " - " + nl.minzoom).join(", ") - ) - /** * A listing which maps the layerId onto the featureSource */ @@ -721,7 +717,7 @@ export default class ThemeViewState implements SpecialVisualizationState { ), current_view: this.currentView, favourite: this.favourites, - summary: this.setupSummaryLayer(maxzoom), + summary: this.featureSummary, } this.closestFeatures.registerSource(specialLayers.favourite, "favourite") diff --git a/src/UI/DownloadFlow/DownloadButton.svelte b/src/UI/DownloadFlow/DownloadButton.svelte index a8d0b1c72..9ad837de9 100644 --- a/src/UI/DownloadFlow/DownloadButton.svelte +++ b/src/UI/DownloadFlow/DownloadButton.svelte @@ -3,10 +3,8 @@ import { ArrowDownTrayIcon } from "@babeard/svelte-heroicons/mini" import Tr from "../Base/Tr.svelte" import Translations from "../i18n/Translations" - import type { FeatureCollection } from "geojson" import Loading from "../Base/Loading.svelte" import { Translation } from "../i18n/Translation" - import DownloadHelper from "./DownloadHelper" import { Utils } from "../../Utils" import type { PriviligedLayerType } from "../../Models/Constants" import { UIEventSource } from "../../Logic/UIEventSource" @@ -16,14 +14,11 @@ export let extension: string export let mimetype: string export let construct: ( - geojsonCleaned: FeatureCollection, title: string, status?: UIEventSource - ) => (Blob | string) | Promise + ) => Promise export let mainText: Translation export let helperText: Translation - export let metaIsIncluded: boolean - let downloadHelper: DownloadHelper = new DownloadHelper(state) const t = Translations.t.general.download @@ -31,30 +26,21 @@ let isError = false let status: UIEventSource = new UIEventSource(undefined) - async function clicked() { isExporting = true + const gpsLayer = state.layerState.filteredLayers.get("gps_location") state.userRelatedState.preferencesAsTags.data["__showTimeSensitiveIcons"] = "no" state.userRelatedState.preferencesAsTags.ping() const gpsIsDisplayed = gpsLayer.isDisplayed.data try { gpsLayer.isDisplayed.setData(false) - const geojson: FeatureCollection = downloadHelper.getCleanGeoJson(metaIsIncluded) const name = state.layout.id const title = `MapComplete_${name}_export_${new Date() .toISOString() .substr(0, 19)}.${extension}` - const promise = construct(geojson, title, status) - let data: Blob | string - if (typeof promise === "string") { - data = promise - } else if (typeof promise["then"] === "function") { - data = await (>promise) - } else { - data = promise - } + const data: Blob | string = await construct(title, status) if (!data) { return } diff --git a/src/UI/DownloadFlow/DownloadHelper.ts b/src/UI/DownloadFlow/DownloadHelper.ts index 80906a1b1..4b61a4cce 100644 --- a/src/UI/DownloadFlow/DownloadHelper.ts +++ b/src/UI/DownloadFlow/DownloadHelper.ts @@ -153,7 +153,7 @@ export default class DownloadHelper { return header + "\n" + elements.join("\n") + "\n" } - public getCleanGeoJsonPerLayer(includeMetaData: boolean): Map { + private getCleanGeoJsonPerLayer(includeMetaData: boolean): Map { const state = this._state const featuresPerLayer = new Map() const neededLayers = state.layout.layers.filter((l) => l.source !== null).map((l) => l.id) @@ -161,6 +161,7 @@ export default class DownloadHelper { for (const neededLayer of neededLayers) { const indexedFeatureSource = state.perLayer.get(neededLayer) + let features = indexedFeatureSource.GetFeaturesWithin(bbox) // The 'indexedFeatureSources' contains _all_ features, they are not filtered yet const filter = state.layerState.filteredLayers.get(neededLayer) diff --git a/src/UI/DownloadFlow/DownloadPanel.svelte b/src/UI/DownloadFlow/DownloadPanel.svelte index 773f4f3e7..2385f6e04 100644 --- a/src/UI/DownloadFlow/DownloadPanel.svelte +++ b/src/UI/DownloadFlow/DownloadPanel.svelte @@ -17,9 +17,16 @@ const downloadHelper = new DownloadHelper(state) let metaIsIncluded = false - const name = state.layout.id - function offerSvg(noSelfIntersectingLines: boolean): string { + let numberOfFeatures = state.featureSummary.totalNumberOfFeatures + + async function getGeojson() { + await state.indexedFeatures.downloadAll() + return downloadHelper.getCleanGeoJson(metaIsIncluded) + } + + async function offerSvg(noSelfIntersectingLines: boolean): Promise { + await state.indexedFeatures.downloadAll() const maindiv = document.getElementById("maindiv") const layers = state.layout.layers.filter((l) => l.source !== null) return downloadHelper.asSvg({ @@ -34,6 +41,8 @@ {#if $isLoading} +{:else if $numberOfFeatures > 100000} + {:else}

@@ -44,20 +53,18 @@ {state} extension="geojson" mimetype="application/vnd.geo+json" - construct={(geojson) => JSON.stringify(geojson)} + construct={async () => JSON.stringify(await getGeojson())} mainText={t.downloadGeojson} helperText={t.downloadGeoJsonHelper} - {metaIsIncluded} /> GeoOperations.toCSV(geojson)} + construct={async () => GeoOperations.toCSV(await getGeojson())} mainText={t.downloadCSV} helperText={t.downloadCSVHelper} - {metaIsIncluded} />