forked from MapComplete/MapComplete
		
	Download button: take advantage of MVT server, download button will now attempt to download everything
This commit is contained in:
		
							parent
							
								
									bccda67e1c
								
							
						
					
					
						commit
						e4eb8d6b52
					
				
					 21 changed files with 453 additions and 353 deletions
				
			
		|  | @ -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?", | ||||
|  |  | |||
|  | @ -5,6 +5,13 @@ import { Feature } from "geojson" | |||
| export interface FeatureSource<T extends Feature = Feature> { | ||||
|     features: Store<T[]> | ||||
| } | ||||
| 
 | ||||
| export interface UpdatableFeatureSource<T extends Feature = Feature> extends FeatureSource<T> { | ||||
|     /** | ||||
|      * Forces an update and downloads the data, even if the feature source is supposed to be active | ||||
|      */ | ||||
|     updateAsync() | ||||
| } | ||||
| export interface WritableFeatureSource<T extends Feature = Feature> extends FeatureSource<T> { | ||||
|     features: UIEventSource<T[]> | ||||
| } | ||||
|  | @ -16,11 +23,10 @@ export interface FeatureSourceForLayer<T extends Feature = Feature> extends Feat | |||
|     readonly layer: FilteredLayer | ||||
| } | ||||
| 
 | ||||
| export interface FeatureSourceForTile <T extends Feature = Feature> extends FeatureSource<T> { | ||||
| export interface FeatureSourceForTile<T extends Feature = Feature> extends FeatureSource<T> { | ||||
|     readonly x: number | ||||
|     readonly y: number | ||||
|     readonly z: number | ||||
| 
 | ||||
| } | ||||
| /** | ||||
|  * A feature source which is aware of the indexes it contains | ||||
|  |  | |||
|  | @ -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<Src extends FeatureSource = FeatureSource> implements IndexedFeatureSource { | ||||
| export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSource> | ||||
|     implements IndexedFeatureSource | ||||
| { | ||||
|     public features: UIEventSource<Feature[]> = new UIEventSource([]) | ||||
|     public readonly featuresById: Store<Map<string, Feature>> | ||||
|     protected readonly _featuresById: UIEventSource<Map<string, Feature>> | ||||
|     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<Src extends FeatureSource = FeatureSour | |||
|                 self.addDataFromSources(sources) | ||||
|             }) | ||||
|         } | ||||
|         this.addDataFromSources(sources) | ||||
|         this._sources = sources | ||||
|         this.addDataFromSources(sources) | ||||
|     } | ||||
| 
 | ||||
|     public addSource(source: Src) { | ||||
|         if (!source) { | ||||
|             return | ||||
|         } | ||||
|         if (!source.features) { | ||||
|             console.error("No source found in", source) | ||||
|         } | ||||
|         this._sources.push(source) | ||||
|         source.features.addCallbackAndRun(() => { | ||||
|             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<Src extends FeatureSource = FeatureSour | |||
|         this._featuresById.setData(all) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class UpdatableFeatureSourceMerger< | ||||
|         Src extends UpdatableFeatureSource = UpdatableFeatureSource | ||||
|     > | ||||
|     extends FeatureSourceMerger<Src> | ||||
|     implements IndexedFeatureSource, UpdatableFeatureSource | ||||
| { | ||||
|     constructor(...sources: Src[]) { | ||||
|         super(...sources) | ||||
|     } | ||||
|     async updateAsync() { | ||||
|         await Promise.all(this._sources.map((src) => src.updateAsync())) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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<Feature[]> | ||||
|     private readonly _features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>(undefined) | ||||
|     public readonly features: Store<Feature[]> = this._features | ||||
|     private readonly seenids: Set<string> | ||||
|     private readonly idKey?: string | ||||
|     private readonly url: string | ||||
|     private readonly layer: LayerConfig | ||||
|     private _isDownloaded = false | ||||
|     private currentlyRunning: Promise<any> | ||||
| 
 | ||||
|     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<string>() | ||||
|         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<Feature[]>([]) | ||||
|         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<void> { | ||||
|         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<Feature[]>, | ||||
|         layer: LayerConfig, | ||||
|         options?: { | ||||
|             maxCacheAgeSec?: number | 300 | ||||
|     private async LoadJSONFrom(options?: { maxCacheAgeSec?: number | 300 }): Promise<Feature[]> { | ||||
|         if (this._isDownloaded) { | ||||
|             return | ||||
|         } | ||||
|     ): Promise<Feature[]> { | ||||
|         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 | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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<boolean> | ||||
| 
 | ||||
|     private readonly supportsForceDownload: UpdatableFeatureSource[] | ||||
| 
 | ||||
|     constructor( | ||||
|         layers: LayerConfig[], | ||||
|         featureSwitches: FeatureSwitchState, | ||||
|  | @ -33,6 +35,8 @@ export default class LayoutSource extends FeatureSourceMerger { | |||
|         mvtAvailableLayers: Set<string>, | ||||
|         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<number>; bounds: Store<BBox> }, | ||||
|         isActive?: Store<boolean> | ||||
|     ): FeatureSource { | ||||
|     ): UpdatableFeatureSource { | ||||
|         return new DynamicMvtileSource(layer, mapProperties, { isActive }) | ||||
|     } | ||||
| 
 | ||||
|  | @ -104,7 +113,7 @@ export default class LayoutSource extends FeatureSourceMerger { | |||
|         layer: LayerConfig, | ||||
|         mapProperties: { zoom: Store<number>; bounds: Store<BBox> }, | ||||
|         isActive?: Store<boolean> | ||||
|     ): 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") | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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<GeojsonFeature<Geometry, { [name: string]: any }>[]> | ||||
|     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<GeojsonFeature<Geometry, { [p: string]: any }>[]>([]) | ||||
|     public readonly x: number | ||||
|     public readonly y: number | ||||
|     public readonly z: number | ||||
|     private currentlyRunning: Promise<any> | ||||
| 
 | ||||
|     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<GeojsonFeature[]> { | ||||
|         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<void> { | ||||
|         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 }[]) { | ||||
|  |  | |||
|  | @ -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<void> { | ||||
|         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) | ||||
|  |  | |||
|  | @ -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<string, any>() | ||||
| 
 | ||||
|     constructor( | ||||
|  | @ -65,7 +65,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | |||
| 
 | ||||
|         const blackList = new Set<string>() | ||||
|         super( | ||||
|            new ImmutableStore(source.geojsonZoomLevel), | ||||
|             new ImmutableStore(source.geojsonZoomLevel), | ||||
|             layer.minzoom, | ||||
|             (zxy) => { | ||||
|                 if (whitelist !== undefined) { | ||||
|  |  | |||
|  | @ -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<number> | ||||
|                      bounds: Store<BBox> | ||||
|                  }, | ||||
|                  options?: { | ||||
|                      isActive?: Store<boolean> | ||||
|                  }) { | ||||
|         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<number> | ||||
|                      bounds: Store<BBox> | ||||
|                  }, | ||||
|                  options?: { | ||||
|                      isActive?: Store<boolean> | ||||
|                  }) { | ||||
|         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<boolean> | ||||
|         }, | ||||
|         } | ||||
|     ) { | ||||
|         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<boolean> | ||||
|         } | ||||
|     ) { | ||||
|         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<number> | ||||
|             bounds: Store<BBox> | ||||
|         }, | ||||
|         options?: { | ||||
|             isActive?: Store<boolean> | ||||
|         } | ||||
|     ) { | ||||
|         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<number> | ||||
|             bounds: Store<BBox> | ||||
|         }, | ||||
|         options?: { | ||||
|             isActive?: Store<boolean> | ||||
|         } | ||||
|     ) { | ||||
|         super( | ||||
|             new PointMvtSource(layer, mapProperties, options), | ||||
|             new LineMvtSource(layer, mapProperties, options), | ||||
|             new PolygonMvtSource(layer, mapProperties, options) | ||||
| 
 | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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<Src> { | ||||
|     private readonly loadedTiles = new Set<number>() | ||||
|     private readonly zDiff: number | ||||
|     private readonly zoomlevel: Store<number> | ||||
|     private readonly constructSource: (tileIndex: number) => Src | ||||
|     private readonly bounds: Store<BBox> | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * @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<number>() | ||||
|         const zDiff = options?.zDiff ?? 0 | ||||
|         this.constructSource = constructSource | ||||
|         this.zoomlevel = zoomlevel | ||||
|         this.zDiff = options?.zDiff ?? 0 | ||||
|         this.bounds = mapProperties.bounds | ||||
| 
 | ||||
|         const neededTiles: Store<number[]> = 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<Src extends UpdatableFeatureSource = UpdatableFeatureSource> | ||||
|     extends DynamicTileSource<Src> | ||||
|     implements UpdatableFeatureSource | ||||
| { | ||||
|     constructor( | ||||
|         zoomlevel: Store<number>, | ||||
|         minzoom: number, | ||||
|         constructSource: (tileIndex: number) => Src, | ||||
|         mapProperties: { | ||||
|             bounds: Store<BBox> | ||||
|             zoom: Store<number> | ||||
|         }, | ||||
|         options?: { | ||||
|             isActive?: Store<boolean> | ||||
|             zDiff?: number | ||||
|         } | ||||
|     ) { | ||||
|         super(zoomlevel, minzoom, constructSource, mapProperties, options) | ||||
|     } | ||||
| 
 | ||||
|     async updateAsync() { | ||||
|         const sources = super.downloadTiles(super.getNeededTileIndices()) | ||||
|         await Promise.all(sources.map((src) => src.updateAsync())) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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<FeatureSourceForTile> { | ||||
| export class LineSourceMerger extends UpdatableDynamicTileSource< | ||||
|     FeatureSourceForTile & UpdatableFeatureSource | ||||
| > { | ||||
|     private readonly _zoomlevel: Store<number> | ||||
| 
 | ||||
|     constructor( | ||||
|         zoomlevel: Store<number>, | ||||
|         minzoom: number, | ||||
|         constructSource: (tileIndex: number) => FeatureSourceForTile, | ||||
|         constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource, | ||||
|         mapProperties: { | ||||
|             bounds: Store<BBox> | ||||
|             zoom: Store<number> | ||||
|         }, | ||||
|         options?: { | ||||
|             isActive?: Store<boolean> | ||||
|         }, | ||||
|         } | ||||
|     ) { | ||||
|         super(zoomlevel, minzoom, constructSource, mapProperties, options) | ||||
|         this._zoomlevel = zoomlevel | ||||
|  | @ -35,33 +36,30 @@ export class LineSourceMerger extends DynamicTileSource<FeatureSourceForTile> { | |||
|         const all: Map<string, Feature<MultiLineString>> = 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<FeatureSourceForTile> { | |||
| 
 | ||||
|         const keys = Array.from(all.keys()) | ||||
|         for (const key of keys) { | ||||
|             all.set(key, <any> GeoOperations.attemptLinearize(<Feature<MultiLineString>>all.get(key))) | ||||
|             all.set( | ||||
|                 key, | ||||
|                 <any>GeoOperations.attemptLinearize(<Feature<MultiLineString>>all.get(key)) | ||||
|             ) | ||||
|         } | ||||
|         const newList = Array.from(all.values()) | ||||
|         this.features.setData(newList) | ||||
|         this._featuresById.setData(all) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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<FeatureSourceForTile> { | ||||
| export class PolygonSourceMerger extends UpdatableDynamicTileSource< | ||||
|     FeatureSourceForTile & UpdatableFeatureSource | ||||
| > { | ||||
|     constructor( | ||||
|         zoomlevel: Store<number>, | ||||
|         minzoom: number, | ||||
|         constructSource: (tileIndex: number) => FeatureSourceForTile, | ||||
|         constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource, | ||||
|         mapProperties: { | ||||
|             bounds: Store<BBox> | ||||
|             zoom: Store<number> | ||||
|         }, | ||||
|         options?: { | ||||
|             isActive?: Store<boolean> | ||||
|         }, | ||||
|         } | ||||
|     ) { | ||||
|         super(zoomlevel, minzoom, constructSource, mapProperties, options) | ||||
|     } | ||||
|  | @ -69,5 +71,4 @@ export class PolygonSourceMerger extends DynamicTileSource<FeatureSourceForTile> | |||
|         this.features.setData(newList) | ||||
|         this._featuresById.setData(all) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -14,6 +14,10 @@ export class SummaryTileSourceRewriter implements FeatureSource { | |||
|     private filteredLayers: FilteredLayer[] | ||||
|     public readonly features: Store<Feature[]> = this._features | ||||
|     private readonly _summarySource: SummaryTileSource | ||||
|     private readonly _totalNumberOfFeatures: UIEventSource<number> = new UIEventSource<number>( | ||||
|         undefined | ||||
|     ) | ||||
|     public readonly totalNumberOfFeatures: Store<number> = this._totalNumberOfFeatures | ||||
|     constructor( | ||||
|         summarySource: SummaryTileSource, | ||||
|         filteredLayers: ReadonlyMap<string, FilteredLayer> | ||||
|  | @ -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: { | ||||
|  |  | |||
|  | @ -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<string, GeoIndexedStoreForLayer> | ||||
|     readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource> | ||||
| 
 | ||||
|  | @ -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(<any>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(<any>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(<any>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") | ||||
|  |  | |||
|  | @ -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<string> | ||||
|   ) => (Blob | string) | Promise<void> | ||||
|   ) => Promise<Blob | string> | ||||
|   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<string> = new UIEventSource<string>(undefined) | ||||
| 
 | ||||
|   async function clicked() { | ||||
|     isExporting = true | ||||
|      | ||||
|     const gpsLayer = state.layerState.filteredLayers.get(<PriviligedLayerType>"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<Blob | string>>promise) | ||||
|       } else { | ||||
|         data = <Blob>promise | ||||
|       } | ||||
|       const data: Blob | string = await construct(title, status) | ||||
|       if (!data) { | ||||
|         return | ||||
|       } | ||||
|  |  | |||
|  | @ -153,7 +153,7 @@ export default class DownloadHelper { | |||
|         return header + "\n" + elements.join("\n") + "\n</svg>" | ||||
|     } | ||||
| 
 | ||||
|     public getCleanGeoJsonPerLayer(includeMetaData: boolean): Map<string, Feature[]> { | ||||
|     private getCleanGeoJsonPerLayer(includeMetaData: boolean): Map<string, Feature[]> { | ||||
|         const state = this._state | ||||
|         const featuresPerLayer = new Map<string, any[]>() | ||||
|         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) | ||||
|  |  | |||
|  | @ -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<string> { | ||||
|     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} | ||||
|   <Loading /> | ||||
| {:else if $numberOfFeatures > 100000} | ||||
|   <Tr cls="alert" t={Translations.t.general.download.toMuch} /> | ||||
| {:else} | ||||
|   <div class="flex w-full flex-col" /> | ||||
|   <h3> | ||||
|  | @ -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} | ||||
|   /> | ||||
| 
 | ||||
|   <DownloadButton | ||||
|     {state} | ||||
|     extension="csv" | ||||
|     mimetype="text/csv" | ||||
|     construct={(geojson) => GeoOperations.toCSV(geojson)} | ||||
|     construct={async () => GeoOperations.toCSV(await getGeojson())} | ||||
|     mainText={t.downloadCSV} | ||||
|     helperText={t.downloadCSVHelper} | ||||
|     {metaIsIncluded} | ||||
|   /> | ||||
| 
 | ||||
|   <label class="mb-8 mt-2"> | ||||
|  | @ -67,7 +74,6 @@ | |||
| 
 | ||||
|   <DownloadButton | ||||
|     {state} | ||||
|     {metaIsIncluded} | ||||
|     extension="svg" | ||||
|     mimetype="image/svg+xml" | ||||
|     mainText={t.downloadAsSvg} | ||||
|  | @ -77,7 +83,6 @@ | |||
| 
 | ||||
|   <DownloadButton | ||||
|     {state} | ||||
|     {metaIsIncluded} | ||||
|     extension="svg" | ||||
|     mimetype="image/svg+xml" | ||||
|     mainText={t.downloadAsSvgLinesOnly} | ||||
|  | @ -87,7 +92,6 @@ | |||
| 
 | ||||
|   <DownloadButton | ||||
|     {state} | ||||
|     {metaIsIncluded} | ||||
|     extension="png" | ||||
|     mimetype="image/png" | ||||
|     mainText={t.downloadAsPng} | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ | |||
|   let t = Translations.t.general.download | ||||
|   const downloadHelper = new DownloadHelper(state) | ||||
| 
 | ||||
|   async function constructPdf(_, title: string, status: UIEventSource<string>) { | ||||
|   async function constructPdf(title: string, status: UIEventSource<string>): Promise<Blob> { | ||||
|     title = | ||||
|       title.substring(0, title.length - 4) + "_" + template.format + "_" + template.orientation | ||||
|     const templateUrls = SvgToPdf.templates[templateName].pages | ||||
|  | @ -33,11 +33,11 @@ | |||
|         console.log("Creating an image for key", key) | ||||
|         if (key === "qr") { | ||||
|           const toShare = window.location.href.split("#")[0] | ||||
|           return new Qr(toShare).toImageElement(parseFloat(width), parseFloat(height)) | ||||
|           return new Qr(toShare).toImageElement(parseFloat(width)) | ||||
|         } | ||||
|         return downloadHelper.createImage(key, width, height) | ||||
|       }, | ||||
|       textSubstitutions: <Record<string, string>>{ | ||||
|       textSubstitutions: <Record<string, string | Translation>>{ | ||||
|         "layout.title": state.layout.title, | ||||
|         layoutid: state.layout.id, | ||||
|         title: state.layout.title, | ||||
|  | @ -61,7 +61,6 @@ | |||
|   construct={constructPdf} | ||||
|   extension="pdf" | ||||
|   helperText={t.downloadAsPdfHelper} | ||||
|   metaIsIncluded={false} | ||||
|   mainText={t.pdf.current_view_generic.Subs({ | ||||
|     orientation: template.orientation, | ||||
|     paper_size: template.format.toUpperCase(), | ||||
|  |  | |||
|  | @ -20,6 +20,8 @@ import { OsmTags } from "../Models/OsmFeature" | |||
| import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource" | ||||
| import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider" | ||||
| import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" | ||||
| import { SummaryTileSourceRewriter } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource" | ||||
| import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource" | ||||
| 
 | ||||
| /** | ||||
|  * The state needed to render a special Visualisation. | ||||
|  | @ -30,12 +32,13 @@ export interface SpecialVisualizationState { | |||
|     readonly featureSwitches: FeatureSwitchState | ||||
| 
 | ||||
|     readonly layerState: LayerState | ||||
|     readonly featureSummary: SummaryTileSourceRewriter | ||||
|     readonly featureProperties: { | ||||
|         getStore(id: string): UIEventSource<Record<string, string>> | ||||
|         trackFeature?(feature: { properties: OsmTags }) | ||||
|     } | ||||
| 
 | ||||
|     readonly indexedFeatures: IndexedFeatureSource | ||||
|     readonly indexedFeatures: IndexedFeatureSource & LayoutSource | ||||
|     /** | ||||
|      * Some features will create a new element that should be displayed. | ||||
|      * These can be injected by appending them to this featuresource (and pinging it) | ||||
|  |  | |||
|  | @ -16,7 +16,6 @@ import mcChanges from "../../src/assets/generated/themes/mapcomplete-changes.jso | |||
| import SvelteUIElement from "./Base/SvelteUIElement" | ||||
| import Filterview from "./BigComponents/Filterview.svelte" | ||||
| import FilteredLayer from "../Models/FilteredLayer" | ||||
| import DownloadButton from "./DownloadFlow/DownloadButton.svelte" | ||||
| import { SubtleButton } from "./Base/SubtleButton" | ||||
| import { GeoOperations } from "../Logic/GeoOperations" | ||||
| import { Polygon } from "geojson" | ||||
|  |  | |||
|  | @ -553,7 +553,7 @@ class SvgToPdfInternals { | |||
| export interface SvgToPdfOptions { | ||||
|     freeComponentId: string | ||||
|     disableMaps?: false | true | ||||
|     textSubstitutions?: Record<string, string> | ||||
|     textSubstitutions?: Record<string, string | Translation> | ||||
|     beforePage?: (i: number) => void | ||||
|     overrideLocation?: { lat: number; lon: number } | ||||
|     disableDataLoading?: boolean | false | ||||
|  | @ -711,9 +711,13 @@ class SvgToPdfPage { | |||
|             this.options.beforePage(i) | ||||
|         } | ||||
|         const self = this | ||||
|         const internal = new SvgToPdfInternals(advancedApi, this, (key) => | ||||
|             self.extractTranslation(key, language) | ||||
|         ) | ||||
|         const internal = new SvgToPdfInternals(advancedApi, this, (key) => { | ||||
|             const tr = self.extractTranslation(key, language) | ||||
|             if (typeof tr === "string") { | ||||
|                 return tr | ||||
|             } | ||||
|             return tr.txt | ||||
|         }) | ||||
|         for (const child of Array.from(this._svgRoot.children)) { | ||||
|             internal.handleElement(<any>child) | ||||
|         } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue