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" |                 "current_view_generic": "Export a PDF off the current view for {paper_size} in {orientation} orientation" | ||||||
|             }, |             }, | ||||||
|             "title": "Download", |             "title": "Download", | ||||||
|  |             "toMuch": "There are to much features to download them all", | ||||||
|             "uploadGpx": "Upload your track to OpenStreetMap" |             "uploadGpx": "Upload your track to OpenStreetMap" | ||||||
|         }, |         }, | ||||||
|         "enableGeolocationForSafari": "Did you not get the popup to ask for geopermission?", |         "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> { | export interface FeatureSource<T extends Feature = Feature> { | ||||||
|     features: Store<T[]> |     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> { | export interface WritableFeatureSource<T extends Feature = Feature> extends FeatureSource<T> { | ||||||
|     features: UIEventSource<T[]> |     features: UIEventSource<T[]> | ||||||
| } | } | ||||||
|  | @ -16,11 +23,10 @@ export interface FeatureSourceForLayer<T extends Feature = Feature> extends Feat | ||||||
|     readonly layer: FilteredLayer |     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 x: number | ||||||
|     readonly y: number |     readonly y: number | ||||||
|     readonly z: number |     readonly z: number | ||||||
| 
 |  | ||||||
| } | } | ||||||
| /** | /** | ||||||
|  * A feature source which is aware of the indexes it contains |  * A feature source which is aware of the indexes it contains | ||||||
|  |  | ||||||
|  | @ -1,18 +1,20 @@ | ||||||
| import { Store, UIEventSource } from "../../UIEventSource" | import { Store, UIEventSource } from "../../UIEventSource" | ||||||
| import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" | import { FeatureSource, IndexedFeatureSource, UpdatableFeatureSource } from "../FeatureSource" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import { Utils } from "../../../Utils" | import { Utils } from "../../../Utils" | ||||||
| import DynamicTileSource from "../TiledFeatureSource/DynamicTileSource" |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * The featureSourceMerger receives complete geometries from various sources. |  * 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 |  * 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 features: UIEventSource<Feature[]> = new UIEventSource([]) | ||||||
|     public readonly featuresById: Store<Map<string, Feature>> |     public readonly featuresById: Store<Map<string, Feature>> | ||||||
|     protected readonly _featuresById: UIEventSource<Map<string, Feature>> |     protected readonly _featuresById: UIEventSource<Map<string, Feature>> | ||||||
|     private readonly _sources: Src[] = [] |     protected readonly _sources: Src[] | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Merges features from different featureSources. |      * 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 |      * 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) |                 self.addDataFromSources(sources) | ||||||
|             }) |             }) | ||||||
|         } |         } | ||||||
|         this.addDataFromSources(sources) |  | ||||||
|         this._sources = sources |         this._sources = sources | ||||||
|  |         this.addDataFromSources(sources) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public addSource(source: Src) { |     public addSource(source: Src) { | ||||||
|         if (!source) { |         if (!source) { | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|  |         if (!source.features) { | ||||||
|  |             console.error("No source found in", source) | ||||||
|  |         } | ||||||
|         this._sources.push(source) |         this._sources.push(source) | ||||||
|         source.features.addCallbackAndRun(() => { |         source.features.addCallbackAndRun(() => { | ||||||
|             this.addDataFromSources(this._sources) |             this.addDataFromSources(this._sources) | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected addDataFromSources(sources: Src[]){ |     protected addDataFromSources(sources: Src[]) { | ||||||
|         this.addData(sources.map(s => s.features.data)) |         this.addData(sources.map((s) => s.features.data)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     protected addData(sources: Feature[][]) { |     protected addData(sources: Feature[][]) { | ||||||
|  | @ -93,3 +98,17 @@ export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSour | ||||||
|         this._featuresById.setData(all) |         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" | import { Tiles } from "../../../Models/TileRange" | ||||||
| 
 | 
 | ||||||
| export default class GeoJsonSource implements FeatureSource { | 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 seenids: Set<string> | ||||||
|     private readonly idKey?: string |     private readonly idKey?: string | ||||||
|  |     private readonly url: string | ||||||
|  |     private readonly layer: LayerConfig | ||||||
|  |     private _isDownloaded = false | ||||||
|  |     private currentlyRunning: Promise<any> | ||||||
| 
 | 
 | ||||||
|     public constructor( |     public constructor( | ||||||
|         layer: LayerConfig, |         layer: LayerConfig, | ||||||
|  | @ -30,6 +35,7 @@ export default class GeoJsonSource implements FeatureSource { | ||||||
|         this.idKey = layer.source.idKey |         this.idKey = layer.source.idKey | ||||||
|         this.seenids = options?.featureIdBlacklist ?? new Set<string>() |         this.seenids = options?.featureIdBlacklist ?? new Set<string>() | ||||||
|         let url = layer.source.geojsonSource.replace("{layer}", layer.id) |         let url = layer.source.geojsonSource.replace("{layer}", layer.id) | ||||||
|  |         this.layer = layer | ||||||
|         let zxy = options?.zxy |         let zxy = options?.zxy | ||||||
|         if (zxy !== undefined) { |         if (zxy !== undefined) { | ||||||
|             let tile_bbox: BBox |             let tile_bbox: BBox | ||||||
|  | @ -57,86 +63,88 @@ export default class GeoJsonSource implements FeatureSource { | ||||||
|                 .replace("{x_min}", "" + bounds.minLon) |                 .replace("{x_min}", "" + bounds.minLon) | ||||||
|                 .replace("{x_max}", "" + bounds.maxLon) |                 .replace("{x_max}", "" + bounds.maxLon) | ||||||
|         } |         } | ||||||
|  |         this.url = url | ||||||
| 
 | 
 | ||||||
|         const eventsource = new UIEventSource<Feature[]>([]) |  | ||||||
|         if (options?.isActive !== undefined) { |         if (options?.isActive !== undefined) { | ||||||
|             options.isActive.addCallbackAndRunD(async (active) => { |             options.isActive.addCallbackAndRunD(async (active) => { | ||||||
|                 if (!active) { |                 if (!active) { | ||||||
|                     return |                     return | ||||||
|                 } |                 } | ||||||
|                 this.LoadJSONFrom(url, eventsource, layer) |                 this.updateAsync() | ||||||
|                     .then((fs) => console.debug("Loaded", fs.length, "features from", url)) |  | ||||||
|                     .catch((err) => console.warn("Could not load ", url, "due to", err)) |  | ||||||
|                 return true // data is loaded, we can safely unregister
 |                 return true // data is loaded, we can safely unregister
 | ||||||
|             }) |             }) | ||||||
|         } else { |         } else { | ||||||
|             this.LoadJSONFrom(url, eventsource, layer) |             this.updateAsync() | ||||||
|                 .then((fs) => console.debug("Loaded", fs.length, "features from", url)) |  | ||||||
|                 .catch((err) => console.warn("Could not load ", url, "due to", err)) |  | ||||||
|         } |         } | ||||||
|         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. |      * Init the download, write into the specified event source for the given layer. | ||||||
|      * Note this method caches the requested geojson for five minutes |      * Note this method caches the requested geojson for five minutes | ||||||
|      */ |      */ | ||||||
|     private async LoadJSONFrom( |     private async LoadJSONFrom(options?: { maxCacheAgeSec?: number | 300 }): Promise<Feature[]> { | ||||||
|         url: string, |         if (this._isDownloaded) { | ||||||
|         eventSource: UIEventSource<Feature[]>, |             return | ||||||
|         layer: LayerConfig, |  | ||||||
|         options?: { |  | ||||||
|             maxCacheAgeSec?: number | 300 |  | ||||||
|         } |         } | ||||||
|     ): Promise<Feature[]> { |         const url = this.url | ||||||
|         const self = this |         try { | ||||||
|         let json = await Utils.downloadJsonCached(url, (options?.maxCacheAgeSec ?? 300) * 1000) |             let json = await Utils.downloadJsonCached(url, (options?.maxCacheAgeSec ?? 300) * 1000) | ||||||
| 
 | 
 | ||||||
|         if (json.features === undefined || json.features === null) { |             if (json.features === undefined || json.features === null) { | ||||||
|             json.features = [] |                 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) |  | ||||||
|             } |             } | ||||||
|             const props = feature.properties | 
 | ||||||
|             for (const key in props) { |             if (this.layer.source.mercatorCrs) { | ||||||
|                 if (props[key] === null) { |                 json = GeoOperations.GeoJsonToWGS84(json) | ||||||
|                     delete props[key] |             } | ||||||
|  | 
 | ||||||
|  |             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") { |                 if (this.idKey !== undefined) { | ||||||
|                     // Make sure all the values are string, it crashes stuff otherwise
 |                     props.id = props[this.idKey] | ||||||
|                     props[key] = JSON.stringify(props[key]) |  | ||||||
|                 } |                 } | ||||||
|  | 
 | ||||||
|  |                 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) { |             this._features.setData(newFeatures) | ||||||
|                 props.id = props[self.idKey] |             this._isDownloaded = true | ||||||
|             } |             return newFeatures | ||||||
| 
 |         } catch (e) { | ||||||
|             if (props.id === undefined) { |             console.warn("Could not load ", url, "due to", e) | ||||||
|                 props.id = url + "/" + i |  | ||||||
|                 feature.id = url + "/" + i |  | ||||||
|                 i++ |  | ||||||
|             } |  | ||||||
|             if (self.seenids.has(props.id)) { |  | ||||||
|                 continue |  | ||||||
|             } |  | ||||||
|             self.seenids.add(props.id) |  | ||||||
|             newFeatures.push(feature) |  | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         eventSource.setData(newFeatures) |  | ||||||
|         return newFeatures |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,17 +1,17 @@ | ||||||
| import GeoJsonSource from "./GeoJsonSource" | import GeoJsonSource from "./GeoJsonSource" | ||||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||||
| import { FeatureSource } from "../FeatureSource" | import { UpdatableFeatureSource } from "../FeatureSource" | ||||||
| import { Or } from "../../Tags/Or" | import { Or } from "../../Tags/Or" | ||||||
| import FeatureSwitchState from "../../State/FeatureSwitchState" | import FeatureSwitchState from "../../State/FeatureSwitchState" | ||||||
| import OverpassFeatureSource from "./OverpassFeatureSource" | import OverpassFeatureSource from "./OverpassFeatureSource" | ||||||
| import { Store, UIEventSource } from "../../UIEventSource" | import { Store, UIEventSource } from "../../UIEventSource" | ||||||
| import OsmFeatureSource from "./OsmFeatureSource" | import OsmFeatureSource from "./OsmFeatureSource" | ||||||
| import FeatureSourceMerger from "./FeatureSourceMerger" |  | ||||||
| import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" | import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" | ||||||
| import { BBox } from "../../BBox" | import { BBox } from "../../BBox" | ||||||
| import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" | import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" | ||||||
| import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource" | import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource" | ||||||
| import DynamicMvtileSource from "../TiledFeatureSource/DynamicMvtTileSource" | import DynamicMvtileSource from "../TiledFeatureSource/DynamicMvtTileSource" | ||||||
|  | import FeatureSourceMerger from "./FeatureSourceMerger" | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * This source will fetch the needed data from various sources for the given layout. |  * 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> |     public readonly isLoading: Store<boolean> | ||||||
| 
 | 
 | ||||||
|  |     private readonly supportsForceDownload: UpdatableFeatureSource[] | ||||||
|  | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         layers: LayerConfig[], |         layers: LayerConfig[], | ||||||
|         featureSwitches: FeatureSwitchState, |         featureSwitches: FeatureSwitchState, | ||||||
|  | @ -33,6 +35,8 @@ export default class LayoutSource extends FeatureSourceMerger { | ||||||
|         mvtAvailableLayers: Set<string>, |         mvtAvailableLayers: Set<string>, | ||||||
|         fullNodeDatabaseSource?: FullNodeDatabaseSource |         fullNodeDatabaseSource?: FullNodeDatabaseSource | ||||||
|     ) { |     ) { | ||||||
|  |         const supportsForceDownload: UpdatableFeatureSource[] = [] | ||||||
|  | 
 | ||||||
|         const { bounds, zoom } = mapProperties |         const { bounds, zoom } = mapProperties | ||||||
|         // remove all 'special' layers
 |         // remove all 'special' layers
 | ||||||
|         layers = layers.filter((layer) => layer.source !== null && layer.source !== undefined) |         layers = layers.filter((layer) => layer.source !== null && layer.source !== undefined) | ||||||
|  | @ -46,7 +50,7 @@ export default class LayoutSource extends FeatureSourceMerger { | ||||||
|                     maxAge: l.maxAgeOfCache, |                     maxAge: l.maxAgeOfCache, | ||||||
|                 }) |                 }) | ||||||
|         ) |         ) | ||||||
|         const mvtSources: FeatureSource[] = osmLayers |         const mvtSources: UpdatableFeatureSource[] = osmLayers | ||||||
|             .filter((f) => mvtAvailableLayers.has(f.id)) |             .filter((f) => mvtAvailableLayers.has(f.id)) | ||||||
|             .map((l) => LayoutSource.setupMvtSource(l, mapProperties, isDisplayed(l.id))) |             .map((l) => LayoutSource.setupMvtSource(l, mapProperties, isDisplayed(l.id))) | ||||||
|         const nonMvtSources = [] |         const nonMvtSources = [] | ||||||
|  | @ -79,24 +83,29 @@ export default class LayoutSource extends FeatureSourceMerger { | ||||||
|                 const loading = overpassSource?.runningQuery?.data || osmApiSource?.isRunning?.data |                 const loading = overpassSource?.runningQuery?.data || osmApiSource?.isRunning?.data | ||||||
|                 isLoading.setData(loading) |                 isLoading.setData(loading) | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|             overpassSource?.runningQuery?.addCallbackAndRun((_) => setIsLoading()) |             overpassSource?.runningQuery?.addCallbackAndRun((_) => setIsLoading()) | ||||||
|             osmApiSource?.isRunning?.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)) |             LayoutSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id)) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         super(...geojsonSources, ...fromCache, ...mvtSources, ...nonMvtSources) |         super(...geojsonSources, ...fromCache, ...mvtSources, ...nonMvtSources) | ||||||
| 
 | 
 | ||||||
|         this.isLoading = isLoading |         this.isLoading = isLoading | ||||||
|  |         supportsForceDownload.push(...geojsonSources) | ||||||
|  |         supportsForceDownload.push(...mvtSources) // Non-mvt sources are handled by overpass
 | ||||||
|  |         this.supportsForceDownload = supportsForceDownload | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static setupMvtSource( |     private static setupMvtSource( | ||||||
|         layer: LayerConfig, |         layer: LayerConfig, | ||||||
|         mapProperties: { zoom: Store<number>; bounds: Store<BBox> }, |         mapProperties: { zoom: Store<number>; bounds: Store<BBox> }, | ||||||
|         isActive?: Store<boolean> |         isActive?: Store<boolean> | ||||||
|     ): FeatureSource { |     ): UpdatableFeatureSource { | ||||||
|         return new DynamicMvtileSource(layer, mapProperties, { isActive }) |         return new DynamicMvtileSource(layer, mapProperties, { isActive }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -104,7 +113,7 @@ export default class LayoutSource extends FeatureSourceMerger { | ||||||
|         layer: LayerConfig, |         layer: LayerConfig, | ||||||
|         mapProperties: { zoom: Store<number>; bounds: Store<BBox> }, |         mapProperties: { zoom: Store<number>; bounds: Store<BBox> }, | ||||||
|         isActive?: Store<boolean> |         isActive?: Store<boolean> | ||||||
|     ): FeatureSource { |     ): UpdatableFeatureSource { | ||||||
|         const source = layer.source |         const source = layer.source | ||||||
|         isActive = mapProperties.zoom.map( |         isActive = mapProperties.zoom.map( | ||||||
|             (z) => (isActive?.data ?? true) && z >= layer.minzoom, |             (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, Geometry } from "geojson" | ||||||
| import { Feature as GeojsonFeature } from "geojson" |  | ||||||
| 
 | 
 | ||||||
| import { Store, UIEventSource } from "../../UIEventSource" | import { Store, UIEventSource } from "../../UIEventSource" | ||||||
| import { FeatureSourceForTile } from "../FeatureSource" | import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" | ||||||
| import Pbf from "pbf" | import Pbf from "pbf" | ||||||
| 
 | 
 | ||||||
| type Coords = [number, number][] | type Coords = [number, number][] | ||||||
|  | @ -205,6 +204,7 @@ class Layer { | ||||||
|             end |             end | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     static _readField(tag, obj, pbf) { |     static _readField(tag, obj, pbf) { | ||||||
|         if (tag === 15) obj.version = pbf.readVarint() |         if (tag === 15) obj.version = pbf.readVarint() | ||||||
|         else if (tag === 1) obj.name = pbf.readString() |         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 === 4) obj.values.push(Value.read(pbf, pbf.readVarint() + pbf.pos)) | ||||||
|         else if (tag === 5) obj.extent = pbf.readVarint() |         else if (tag === 5) obj.extent = pbf.readVarint() | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     public static write(obj, pbf) { |     public static write(obj, pbf) { | ||||||
|         if (obj.version) pbf.writeVarintField(15, obj.version) |         if (obj.version) pbf.writeVarintField(15, obj.version) | ||||||
|         if (obj.name) pbf.writeStringField(1, obj.name) |         if (obj.name) pbf.writeStringField(1, obj.name) | ||||||
|  | @ -230,12 +231,14 @@ class Feature { | ||||||
|     static read(pbf, end) { |     static read(pbf, end) { | ||||||
|         return pbf.readFields(Feature._readField, { id: 0, tags: [], type: 0, geometry: [] }, end) |         return pbf.readFields(Feature._readField, { id: 0, tags: [], type: 0, geometry: [] }, end) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     static _readField(tag, obj, pbf) { |     static _readField(tag, obj, pbf) { | ||||||
|         if (tag === 1) obj.id = pbf.readVarint() |         if (tag === 1) obj.id = pbf.readVarint() | ||||||
|         else if (tag === 2) pbf.readPackedVarint(obj.tags) |         else if (tag === 2) pbf.readPackedVarint(obj.tags) | ||||||
|         else if (tag === 3) obj.type = pbf.readVarint() |         else if (tag === 3) obj.type = pbf.readVarint() | ||||||
|         else if (tag === 4) pbf.readPackedVarint(obj.geometry) |         else if (tag === 4) pbf.readPackedVarint(obj.geometry) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     public static write(obj, pbf) { |     public static write(obj, pbf) { | ||||||
|         if (obj.id) pbf.writeVarintField(1, obj.id) |         if (obj.id) pbf.writeVarintField(1, obj.id) | ||||||
|         if (obj.tags) pbf.writePackedVarint(2, obj.tags) |         if (obj.tags) pbf.writePackedVarint(2, obj.tags) | ||||||
|  | @ -260,6 +263,7 @@ class Value { | ||||||
|             end |             end | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     static _readField = function (tag, obj, pbf) { |     static _readField = function (tag, obj, pbf) { | ||||||
|         if (tag === 1) obj.string_value = pbf.readString() |         if (tag === 1) obj.string_value = pbf.readString() | ||||||
|         else if (tag === 2) obj.float_value = pbf.readFloat() |         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 === 6) obj.sint_value = pbf.readSVarint() | ||||||
|         else if (tag === 7) obj.bool_value = pbf.readBoolean() |         else if (tag === 7) obj.bool_value = pbf.readBoolean() | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     public static write(obj, pbf) { |     public static write(obj, pbf) { | ||||||
|         if (obj.string_value) pbf.writeStringField(1, obj.string_value) |         if (obj.string_value) pbf.writeStringField(1, obj.string_value) | ||||||
|         if (obj.float_value) pbf.writeFloatField(2, obj.float_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) |         if (obj.bool_value) pbf.writeBooleanField(7, obj.bool_value) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
| class Tile { | class Tile { | ||||||
|     // code generated by pbf v3.2.1
 |     // 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 = { |     static GeomType = { | ||||||
|         UNKNOWN: { |         UNKNOWN: { | ||||||
|             value: 0, |             value: 0, | ||||||
|  | @ -312,10 +306,27 @@ class Tile { | ||||||
|             options: {}, |             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 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 _url: string | ||||||
|     private readonly _layerName: string |     private readonly _layerName: string | ||||||
|     private readonly _features: UIEventSource< |     private readonly _features: UIEventSource< | ||||||
|  | @ -326,9 +337,7 @@ export default class MvtSource implements FeatureSourceForTile { | ||||||
|             } |             } | ||||||
|         >[] |         >[] | ||||||
|     > = new UIEventSource<GeojsonFeature<Geometry, { [p: string]: any }>[]>([]) |     > = new UIEventSource<GeojsonFeature<Geometry, { [p: string]: any }>[]>([]) | ||||||
|     public readonly x: number |     private currentlyRunning: Promise<any> | ||||||
|     public readonly y: number |  | ||||||
|     public readonly z: number |  | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         url: string, |         url: string, | ||||||
|  | @ -343,7 +352,7 @@ export default class MvtSource implements FeatureSourceForTile { | ||||||
|         this.x = x |         this.x = x | ||||||
|         this.y = y |         this.y = y | ||||||
|         this.z = z |         this.z = z | ||||||
|         this.downloadSync() |         this.updateAsync() | ||||||
|         this.features = this._features.map( |         this.features = this._features.map( | ||||||
|             (fs) => { |             (fs) => { | ||||||
|                 if (fs === undefined || isActive?.data === false) { |                 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: { |     private getValue(v: { | ||||||
|         // Exactly one of these values must be present in a valid message
 |         // Exactly one of these values must be present in a valid message
 | ||||||
|         string_value?: string |         string_value?: string | ||||||
|  | @ -389,47 +405,37 @@ export default class MvtSource implements FeatureSourceForTile { | ||||||
|         return undefined |         return undefined | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private downloadSync() { |     private async download(): Promise<void> { | ||||||
|         this.download() |         try { | ||||||
|             .then((d) => { |             const result = await fetch(this._url) | ||||||
|                 if (d.length === 0) { |             if (result.status !== 200) { | ||||||
|                     return |                 console.error("Could not download tile " + this._url) | ||||||
|                 } |                 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" |  | ||||||
|             } |             } | ||||||
|             layer = layers.find((l) => l.name === this._layerName) |             const buffer = await result.arrayBuffer() | ||||||
|         } |             const data = Tile.read(new Pbf(buffer), undefined) | ||||||
|         if (!layer) { |             const layers = data.layers | ||||||
|             return [] |             let layer = data.layers[0] | ||||||
|         } |             if (layers.length > 1) { | ||||||
|         const builder = new MvtFeatureBuilder(layer.extent, this.x, this.y, this.z) |                 if (!this._layerName) { | ||||||
|         const features: GeojsonFeature[] = [] |                     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) { |             for (const feature of layer.features) { | ||||||
|             const properties = this.inflateProperties(feature.tags, layer.keys, layer.values) |                 const properties = this.inflateProperties(feature.tags, layer.keys, layer.values) | ||||||
|             features.push(builder.toGeoJson(feature.geometry, feature.type, properties)) |                 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 }[]) { |     private inflateProperties(tags: number[], keys: string[], values: { string_value: string }[]) { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import { FeatureSource } from "../FeatureSource" | import { FeatureSource, UpdatableFeatureSource } from "../FeatureSource" | ||||||
| import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | ||||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||||
| import { Or } from "../../Tags/Or" | import { Or } from "../../Tags/Or" | ||||||
|  | @ -12,7 +12,7 @@ import { BBox } from "../../BBox" | ||||||
|  * A wrapper around the 'Overpass'-object. |  * A wrapper around the 'Overpass'-object. | ||||||
|  * It has more logic and will automatically fetch the data for the right bbox and the active layers |  * 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 |      * The last loaded features, as geojson | ||||||
|      */ |      */ | ||||||
|  | @ -99,21 +99,15 @@ export default class OverpassFeatureSource implements FeatureSource { | ||||||
|         ) { |         ) { | ||||||
|             return undefined |             return undefined | ||||||
|         } |         } | ||||||
|         const result = await this.updateAsync() |         await this.updateAsync() | ||||||
|         if (!result) { |  | ||||||
|             return |  | ||||||
|         } |  | ||||||
|         const [bounds, _, __] = result |  | ||||||
|         this._lastQueryBBox = bounds |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Download the relevant data from overpass. Attempt to use a different server; only downloads the relevant layers |      * Download the relevant data from overpass. Attempt to use a different server; only downloads the relevant layers | ||||||
|      * @private |      * @private | ||||||
|      */ |      */ | ||||||
|     private async updateAsync(): Promise<[BBox, Date, LayerConfig[]]> { |     public async updateAsync(): Promise<void> { | ||||||
|         let data: any = undefined |         let data: any = undefined | ||||||
|         let date: Date = undefined |  | ||||||
|         let lastUsed = 0 |         let lastUsed = 0 | ||||||
| 
 | 
 | ||||||
|         const layersToDownload = [] |         const layersToDownload = [] | ||||||
|  | @ -172,7 +166,7 @@ export default class OverpassFeatureSource implements FeatureSource { | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 this.runningQuery.setData(true) |                 this.runningQuery.setData(true) | ||||||
|                 ;[data, date] = await overpass.queryGeoJson(bounds) |                 data = await overpass.queryGeoJson(bounds)[0] | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 self.retries.data++ |                 self.retries.data++ | ||||||
|                 self.retries.ping() |                 self.retries.ping() | ||||||
|  | @ -205,10 +199,9 @@ export default class OverpassFeatureSource implements FeatureSource { | ||||||
| 
 | 
 | ||||||
|             console.log("Overpass returned", data.features.length, "features") |             console.log("Overpass returned", data.features.length, "features") | ||||||
|             self.features.setData(data.features) |             self.features.setData(data.features) | ||||||
|             return [bounds, date, layersToDownload] |             this._lastQueryBBox = bounds | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error("Got the overpass response, but could not process it: ", e, e.stack) |             console.error("Got the overpass response, but could not process it: ", e, e.stack) | ||||||
|             return undefined |  | ||||||
|         } finally { |         } finally { | ||||||
|             self.retries.setData(0) |             self.retries.setData(0) | ||||||
|             self.runningQuery.setData(false) |             self.runningQuery.setData(false) | ||||||
|  |  | ||||||
|  | @ -1,11 +1,11 @@ | ||||||
| import { ImmutableStore, Store } from "../../UIEventSource" | import { ImmutableStore, Store } from "../../UIEventSource" | ||||||
| import DynamicTileSource from "./DynamicTileSource" | import { UpdatableDynamicTileSource } from "./DynamicTileSource" | ||||||
| import { Utils } from "../../../Utils" | import { Utils } from "../../../Utils" | ||||||
| import GeoJsonSource from "../Sources/GeoJsonSource" | import GeoJsonSource from "../Sources/GeoJsonSource" | ||||||
| import { BBox } from "../../BBox" | import { BBox } from "../../BBox" | ||||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | 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>() |     private static whitelistCache = new Map<string, any>() | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|  | @ -65,7 +65,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||||
| 
 | 
 | ||||||
|         const blackList = new Set<string>() |         const blackList = new Set<string>() | ||||||
|         super( |         super( | ||||||
|            new ImmutableStore(source.geojsonZoomLevel), |             new ImmutableStore(source.geojsonZoomLevel), | ||||||
|             layer.minzoom, |             layer.minzoom, | ||||||
|             (zxy) => { |             (zxy) => { | ||||||
|                 if (whitelist !== undefined) { |                 if (whitelist !== undefined) { | ||||||
|  |  | ||||||
|  | @ -1,78 +1,16 @@ | ||||||
| import { Store } from "../../UIEventSource" | import { Store } from "../../UIEventSource" | ||||||
| import DynamicTileSource from "./DynamicTileSource" | import { UpdatableDynamicTileSource } from "./DynamicTileSource" | ||||||
| import { Utils } from "../../../Utils" | import { Utils } from "../../../Utils" | ||||||
| import { BBox } from "../../BBox" | import { BBox } from "../../BBox" | ||||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" | ||||||
| import MvtSource from "../Sources/MvtSource" | import MvtSource from "../Sources/MvtSource" | ||||||
| import { Tiles } from "../../../Models/TileRange" | import { Tiles } from "../../../Models/TileRange" | ||||||
| import Constants from "../../../Models/Constants" | import Constants from "../../../Models/Constants" | ||||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | import { UpdatableFeatureSourceMerger } from "../Sources/FeatureSourceMerger" | ||||||
| import { LineSourceMerger } from "./LineSourceMerger" | import { LineSourceMerger } from "./LineSourceMerger" | ||||||
| import { PolygonSourceMerger } from "./PolygonSourceMerger" | import { PolygonSourceMerger } from "./PolygonSourceMerger" | ||||||
| 
 | 
 | ||||||
| 
 | class PolygonMvtSource extends 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 { |  | ||||||
| 
 |  | ||||||
|     constructor( |     constructor( | ||||||
|         layer: LayerConfig, |         layer: LayerConfig, | ||||||
|         mapProperties: { |         mapProperties: { | ||||||
|  | @ -81,31 +19,32 @@ class PointMvtSource extends DynamicTileSource { | ||||||
|         }, |         }, | ||||||
|         options?: { |         options?: { | ||||||
|             isActive?: Store<boolean> |             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( |         super( | ||||||
|             roundedZoom, |             roundedZoom, | ||||||
|             layer.minzoom, |             layer.minzoom, | ||||||
|             (zxy) => { |             (zxy) => { | ||||||
|                 const [z, x, y] = Tiles.tile_from_index(zxy) |                 const [z, x, y] = Tiles.tile_from_index(zxy) | ||||||
|                 const url = Utils.SubstituteKeys(Constants.VectorTileServer, |                 const url = Utils.SubstituteKeys(Constants.VectorTileServer, { | ||||||
|                     { |                     z, | ||||||
|                         z, x, y, layer: layer.id, |                     x, | ||||||
|                         type: "pois", |                     y, | ||||||
|                     }) |                     layer: layer.id, | ||||||
|  |                     type: "polygons", | ||||||
|  |                 }) | ||||||
|                 return new MvtSource(url, x, y, z) |                 return new MvtSource(url, x, y, z) | ||||||
|             }, |             }, | ||||||
|             mapProperties, |             mapProperties, | ||||||
|             { |             { | ||||||
|                 isActive: options?.isActive, |                 isActive: options?.isActive, | ||||||
|             }, |             } | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class DynamicMvtileSource extends FeatureSourceMerger { | class LineMvtSource extends LineSourceMerger { | ||||||
| 
 |  | ||||||
|     constructor( |     constructor( | ||||||
|         layer: LayerConfig, |         layer: LayerConfig, | ||||||
|         mapProperties: { |         mapProperties: { | ||||||
|  | @ -114,13 +53,80 @@ export default class DynamicMvtileSource extends FeatureSourceMerger { | ||||||
|         }, |         }, | ||||||
|         options?: { |         options?: { | ||||||
|             isActive?: Store<boolean> |             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( |         super( | ||||||
|             new PointMvtSource(layer, mapProperties, options), |             new PointMvtSource(layer, mapProperties, options), | ||||||
|             new LineMvtSource(layer, mapProperties, options), |             new LineMvtSource(layer, mapProperties, options), | ||||||
|             new PolygonMvtSource(layer, mapProperties, options) |             new PolygonMvtSource(layer, mapProperties, options) | ||||||
| 
 |  | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import { Store, Stores } from "../../UIEventSource" | import { Store, Stores } from "../../UIEventSource" | ||||||
| import { Tiles } from "../../../Models/TileRange" | import { Tiles } from "../../../Models/TileRange" | ||||||
| import { BBox } from "../../BBox" | import { BBox } from "../../BBox" | ||||||
| import { FeatureSource } from "../FeatureSource" | import { FeatureSource, UpdatableFeatureSource } from "../FeatureSource" | ||||||
| import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | ||||||
| 
 | 
 | ||||||
| /*** | /*** | ||||||
|  | @ -11,6 +11,12 @@ import FeatureSourceMerger from "../Sources/FeatureSourceMerger" | ||||||
| export default class DynamicTileSource< | export default class DynamicTileSource< | ||||||
|     Src extends FeatureSource = FeatureSource |     Src extends FeatureSource = FeatureSource | ||||||
| > extends FeatureSourceMerger<Src> { | > 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 |      * @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() |         super() | ||||||
|         const loadedTiles = new Set<number>() |         this.constructSource = constructSource | ||||||
|         const zDiff = options?.zDiff ?? 0 |         this.zoomlevel = zoomlevel | ||||||
|  |         this.zDiff = options?.zDiff ?? 0 | ||||||
|  |         this.bounds = mapProperties.bounds | ||||||
|  | 
 | ||||||
|         const neededTiles: Store<number[]> = Stores.ListStabilized( |         const neededTiles: Store<number[]> = Stores.ListStabilized( | ||||||
|             mapProperties.bounds |             mapProperties.bounds | ||||||
|                 .mapD( |                 .mapD(() => { | ||||||
|                     (bounds) => { |                     if (options?.isActive && !options?.isActive.data) { | ||||||
|                         if (options?.isActive && !options?.isActive.data) { |                         return undefined | ||||||
|                             return undefined |                     } | ||||||
|                         } |  | ||||||
| 
 | 
 | ||||||
|                         if (mapProperties.zoom.data < minzoom) { |                     if (mapProperties.zoom.data < minzoom) { | ||||||
|                             return undefined |                         return undefined | ||||||
|                         } |                     } | ||||||
|                         const z = Math.floor(zoomlevel.data) + zDiff |                     return this.getNeededTileIndices() | ||||||
|                         const tileRange = Tiles.TileRangeBetween( |                 }, [options?.isActive, mapProperties.zoom]) | ||||||
|                             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] |  | ||||||
|                 ) |  | ||||||
|                 .stabilized(250) |                 .stabilized(250) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         neededTiles.addCallbackAndRunD((neededIndexes) => { |         neededTiles.addCallbackAndRunD((neededIndexes) => this.downloadTiles(neededIndexes)) | ||||||
|             for (const neededIndex of neededIndexes) { |     } | ||||||
|                 loadedTiles.add(neededIndex) | 
 | ||||||
|                 super.addSource(constructSource(neededIndex)) |     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 { Store } from "../../UIEventSource" | ||||||
| import { BBox } from "../../BBox" | import { BBox } from "../../BBox" | ||||||
| import { Utils } from "../../../Utils" | import { Utils } from "../../../Utils" | ||||||
| import { Feature, LineString, MultiLineString, Position } from "geojson" | import { Feature, MultiLineString, Position } from "geojson" | ||||||
| import { Tiles } from "../../../Models/TileRange" |  | ||||||
| import { GeoOperations } from "../../GeoOperations" | 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. |  * The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together. | ||||||
|  * This is used to reconstruct polygons of vector tiles |  * 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> |     private readonly _zoomlevel: Store<number> | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         zoomlevel: Store<number>, |         zoomlevel: Store<number>, | ||||||
|         minzoom: number, |         minzoom: number, | ||||||
|         constructSource: (tileIndex: number) => FeatureSourceForTile, |         constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource, | ||||||
|         mapProperties: { |         mapProperties: { | ||||||
|             bounds: Store<BBox> |             bounds: Store<BBox> | ||||||
|             zoom: Store<number> |             zoom: Store<number> | ||||||
|         }, |         }, | ||||||
|         options?: { |         options?: { | ||||||
|             isActive?: Store<boolean> |             isActive?: Store<boolean> | ||||||
|         }, |         } | ||||||
|     ) { |     ) { | ||||||
|         super(zoomlevel, minzoom, constructSource, mapProperties, options) |         super(zoomlevel, minzoom, constructSource, mapProperties, options) | ||||||
|         this._zoomlevel = zoomlevel |         this._zoomlevel = zoomlevel | ||||||
|  | @ -35,33 +36,30 @@ export class LineSourceMerger extends DynamicTileSource<FeatureSourceForTile> { | ||||||
|         const all: Map<string, Feature<MultiLineString>> = new Map() |         const all: Map<string, Feature<MultiLineString>> = new Map() | ||||||
|         const currentZoom = this._zoomlevel?.data ?? 0 |         const currentZoom = this._zoomlevel?.data ?? 0 | ||||||
|         for (const source of sources) { |         for (const source of sources) { | ||||||
|             if(source.z != currentZoom){ |             if (source.z != currentZoom) { | ||||||
|                 continue |                 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) { |             for (const f of source.features.data) { | ||||||
|                 const id = f.properties.id |                 const id = f.properties.id | ||||||
|                 const coordinates : Position[][] = [] |                 const coordinates: Position[][] = [] | ||||||
|                 if(f.geometry.type === "LineString"){ |                 if (f.geometry.type === "LineString") { | ||||||
|                     coordinates.push(f.geometry.coordinates) |                     coordinates.push(f.geometry.coordinates) | ||||||
|                 }else if(f.geometry.type === "MultiLineString"){ |                 } else if (f.geometry.type === "MultiLineString") { | ||||||
|                     coordinates.push(...f.geometry.coordinates) |                     coordinates.push(...f.geometry.coordinates) | ||||||
|                 }else { |                 } else { | ||||||
|                     console.error("Invalid geometry type:", f.geometry.type) |                     console.error("Invalid geometry type:", f.geometry.type) | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 const oldV = all.get(id) |                 const oldV = all.get(id) | ||||||
|                 if(!oldV){ |                 if (!oldV) { | ||||||
| 
 |                     all.set(id, { | ||||||
|                 all.set(id, { |                         type: "Feature", | ||||||
|                     type: "Feature", |                         properties: f.properties, | ||||||
|                     properties: f.properties, |                         geometry: { | ||||||
|                     geometry:{ |                             type: "MultiLineString", | ||||||
|                         type:"MultiLineString", |                             coordinates, | ||||||
|                         coordinates |                         }, | ||||||
|                     } |                     }) | ||||||
|                 }) |  | ||||||
|                     continue |                     continue | ||||||
|                 } |                 } | ||||||
|                 oldV.geometry.coordinates.push(...coordinates) |                 oldV.geometry.coordinates.push(...coordinates) | ||||||
|  | @ -70,11 +68,13 @@ export class LineSourceMerger extends DynamicTileSource<FeatureSourceForTile> { | ||||||
| 
 | 
 | ||||||
|         const keys = Array.from(all.keys()) |         const keys = Array.from(all.keys()) | ||||||
|         for (const key of 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()) |         const newList = Array.from(all.values()) | ||||||
|         this.features.setData(newList) |         this.features.setData(newList) | ||||||
|         this._featuresById.setData(all) |         this._featuresById.setData(all) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,27 +1,29 @@ | ||||||
| import { FeatureSourceForTile } from "../FeatureSource" | import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource" | ||||||
| import { Store } from "../../UIEventSource" | import { Store } from "../../UIEventSource" | ||||||
| import { BBox } from "../../BBox" | import { BBox } from "../../BBox" | ||||||
| import { Utils } from "../../../Utils" | import { Utils } from "../../../Utils" | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import { GeoOperations } from "../../GeoOperations" | 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. |  * The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together. | ||||||
|  * This is used to reconstruct polygons of vector tiles |  * This is used to reconstruct polygons of vector tiles | ||||||
|  */ |  */ | ||||||
| export class PolygonSourceMerger extends DynamicTileSource<FeatureSourceForTile> { | export class PolygonSourceMerger extends UpdatableDynamicTileSource< | ||||||
|  |     FeatureSourceForTile & UpdatableFeatureSource | ||||||
|  | > { | ||||||
|     constructor( |     constructor( | ||||||
|         zoomlevel: Store<number>, |         zoomlevel: Store<number>, | ||||||
|         minzoom: number, |         minzoom: number, | ||||||
|         constructSource: (tileIndex: number) => FeatureSourceForTile, |         constructSource: (tileIndex: number) => FeatureSourceForTile & UpdatableFeatureSource, | ||||||
|         mapProperties: { |         mapProperties: { | ||||||
|             bounds: Store<BBox> |             bounds: Store<BBox> | ||||||
|             zoom: Store<number> |             zoom: Store<number> | ||||||
|         }, |         }, | ||||||
|         options?: { |         options?: { | ||||||
|             isActive?: Store<boolean> |             isActive?: Store<boolean> | ||||||
|         }, |         } | ||||||
|     ) { |     ) { | ||||||
|         super(zoomlevel, minzoom, constructSource, mapProperties, options) |         super(zoomlevel, minzoom, constructSource, mapProperties, options) | ||||||
|     } |     } | ||||||
|  | @ -69,5 +71,4 @@ export class PolygonSourceMerger extends DynamicTileSource<FeatureSourceForTile> | ||||||
|         this.features.setData(newList) |         this.features.setData(newList) | ||||||
|         this._featuresById.setData(all) |         this._featuresById.setData(all) | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -14,6 +14,10 @@ export class SummaryTileSourceRewriter implements FeatureSource { | ||||||
|     private filteredLayers: FilteredLayer[] |     private filteredLayers: FilteredLayer[] | ||||||
|     public readonly features: Store<Feature[]> = this._features |     public readonly features: Store<Feature[]> = this._features | ||||||
|     private readonly _summarySource: SummaryTileSource |     private readonly _summarySource: SummaryTileSource | ||||||
|  |     private readonly _totalNumberOfFeatures: UIEventSource<number> = new UIEventSource<number>( | ||||||
|  |         undefined | ||||||
|  |     ) | ||||||
|  |     public readonly totalNumberOfFeatures: Store<number> = this._totalNumberOfFeatures | ||||||
|     constructor( |     constructor( | ||||||
|         summarySource: SummaryTileSource, |         summarySource: SummaryTileSource, | ||||||
|         filteredLayers: ReadonlyMap<string, FilteredLayer> |         filteredLayers: ReadonlyMap<string, FilteredLayer> | ||||||
|  | @ -31,6 +35,7 @@ export class SummaryTileSourceRewriter implements FeatureSource { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private update() { |     private update() { | ||||||
|  |         let fullTotal = 0 | ||||||
|         const newFeatures: Feature[] = [] |         const newFeatures: Feature[] = [] | ||||||
|         const layersToCount = this.filteredLayers.filter((fl) => fl.isDisplayed.data) |         const layersToCount = this.filteredLayers.filter((fl) => fl.isDisplayed.data) | ||||||
|         const bitmap = layersToCount.map((l) => (l.isDisplayed.data ? "1" : "0")).join("") |         const bitmap = layersToCount.map((l) => (l.isDisplayed.data ? "1" : "0")).join("") | ||||||
|  | @ -42,10 +47,17 @@ export class SummaryTileSourceRewriter implements FeatureSource { | ||||||
|             } |             } | ||||||
|             newFeatures.push({ |             newFeatures.push({ | ||||||
|                 ...f, |                 ...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._features.setData(newFeatures) | ||||||
|  |         this._totalNumberOfFeatures.setData(fullTotal) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -94,7 +106,7 @@ export class SummaryTileSource extends DynamicTileSource { | ||||||
|                     } |                     } | ||||||
|                     const lat = counts["lat"] |                     const lat = counts["lat"] | ||||||
|                     const lon = counts["lon"] |                     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)) |                     const tileBbox = new BBox(Tiles.tile_bounds_lon_lat(z, x, y)) | ||||||
|                     if (!tileBbox.contains([lon, lat])) { |                     if (!tileBbox.contains([lon, lat])) { | ||||||
|                         console.error( |                         console.error( | ||||||
|  | @ -116,6 +128,7 @@ export class SummaryTileSource extends DynamicTileSource { | ||||||
|                                 summary: "yes", |                                 summary: "yes", | ||||||
|                                 ...counts, |                                 ...counts, | ||||||
|                                 total, |                                 total, | ||||||
|  |                                 total_metric: Utils.numberWithMetrixPrefix(total), | ||||||
|                                 layers: layersSummed, |                                 layers: layersSummed, | ||||||
|                             }, |                             }, | ||||||
|                             geometry: { |                             geometry: { | ||||||
|  |  | ||||||
|  | @ -114,6 +114,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|     readonly closestFeatures: NearbyFeatureSource |     readonly closestFeatures: NearbyFeatureSource | ||||||
|     readonly newFeatures: WritableFeatureSource |     readonly newFeatures: WritableFeatureSource | ||||||
|     readonly layerState: LayerState |     readonly layerState: LayerState | ||||||
|  |     readonly featureSummary: SummaryTileSourceRewriter | ||||||
|     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> |     readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | ||||||
|     readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource> |     readonly perLayerFiltered: ReadonlyMap<string, FilteringFeatureSource> | ||||||
| 
 | 
 | ||||||
|  | @ -378,6 +379,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         ) |         ) | ||||||
|         this.favourites = new FavouritesFeatureSource(this) |         this.favourites = new FavouritesFeatureSource(this) | ||||||
| 
 | 
 | ||||||
|  |         this.featureSummary = this.setupSummaryLayer() | ||||||
|         this.initActors() |         this.initActors() | ||||||
|         this.drawSpecialLayers() |         this.drawSpecialLayers() | ||||||
|         this.initHotkeys() |         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( |         const layers = this.layout.layers.filter( | ||||||
|             (l) => |             (l) => | ||||||
|                 Constants.priviliged_layers.indexOf(<any>l.id) < 0 && |                 Constants.priviliged_layers.indexOf(<any>l.id) < 0 && | ||||||
|  | @ -684,22 +696,6 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|     private drawSpecialLayers() { |     private drawSpecialLayers() { | ||||||
|         type AddedByDefaultTypes = (typeof Constants.added_by_default)[number] |         type AddedByDefaultTypes = (typeof Constants.added_by_default)[number] | ||||||
|         const empty = [] |         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 |          * A listing which maps the layerId onto the featureSource | ||||||
|          */ |          */ | ||||||
|  | @ -721,7 +717,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             ), |             ), | ||||||
|             current_view: this.currentView, |             current_view: this.currentView, | ||||||
|             favourite: this.favourites, |             favourite: this.favourites, | ||||||
|             summary: this.setupSummaryLayer(maxzoom), |             summary: this.featureSummary, | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.closestFeatures.registerSource(specialLayers.favourite, "favourite") |         this.closestFeatures.registerSource(specialLayers.favourite, "favourite") | ||||||
|  |  | ||||||
|  | @ -3,10 +3,8 @@ | ||||||
|   import { ArrowDownTrayIcon } from "@babeard/svelte-heroicons/mini" |   import { ArrowDownTrayIcon } from "@babeard/svelte-heroicons/mini" | ||||||
|   import Tr from "../Base/Tr.svelte" |   import Tr from "../Base/Tr.svelte" | ||||||
|   import Translations from "../i18n/Translations" |   import Translations from "../i18n/Translations" | ||||||
|   import type { FeatureCollection } from "geojson" |  | ||||||
|   import Loading from "../Base/Loading.svelte" |   import Loading from "../Base/Loading.svelte" | ||||||
|   import { Translation } from "../i18n/Translation" |   import { Translation } from "../i18n/Translation" | ||||||
|   import DownloadHelper from "./DownloadHelper" |  | ||||||
|   import { Utils } from "../../Utils" |   import { Utils } from "../../Utils" | ||||||
|   import type { PriviligedLayerType } from "../../Models/Constants" |   import type { PriviligedLayerType } from "../../Models/Constants" | ||||||
|   import { UIEventSource } from "../../Logic/UIEventSource" |   import { UIEventSource } from "../../Logic/UIEventSource" | ||||||
|  | @ -16,14 +14,11 @@ | ||||||
|   export let extension: string |   export let extension: string | ||||||
|   export let mimetype: string |   export let mimetype: string | ||||||
|   export let construct: ( |   export let construct: ( | ||||||
|     geojsonCleaned: FeatureCollection, |  | ||||||
|     title: string, |     title: string, | ||||||
|     status?: UIEventSource<string> |     status?: UIEventSource<string> | ||||||
|   ) => (Blob | string) | Promise<void> |   ) => Promise<Blob | string> | ||||||
|   export let mainText: Translation |   export let mainText: Translation | ||||||
|   export let helperText: Translation |   export let helperText: Translation | ||||||
|   export let metaIsIncluded: boolean |  | ||||||
|   let downloadHelper: DownloadHelper = new DownloadHelper(state) |  | ||||||
| 
 | 
 | ||||||
|   const t = Translations.t.general.download |   const t = Translations.t.general.download | ||||||
| 
 | 
 | ||||||
|  | @ -31,30 +26,21 @@ | ||||||
|   let isError = false |   let isError = false | ||||||
| 
 | 
 | ||||||
|   let status: UIEventSource<string> = new UIEventSource<string>(undefined) |   let status: UIEventSource<string> = new UIEventSource<string>(undefined) | ||||||
| 
 |  | ||||||
|   async function clicked() { |   async function clicked() { | ||||||
|     isExporting = true |     isExporting = true | ||||||
|  |      | ||||||
|     const gpsLayer = state.layerState.filteredLayers.get(<PriviligedLayerType>"gps_location") |     const gpsLayer = state.layerState.filteredLayers.get(<PriviligedLayerType>"gps_location") | ||||||
|     state.userRelatedState.preferencesAsTags.data["__showTimeSensitiveIcons"] = "no" |     state.userRelatedState.preferencesAsTags.data["__showTimeSensitiveIcons"] = "no" | ||||||
|     state.userRelatedState.preferencesAsTags.ping() |     state.userRelatedState.preferencesAsTags.ping() | ||||||
|     const gpsIsDisplayed = gpsLayer.isDisplayed.data |     const gpsIsDisplayed = gpsLayer.isDisplayed.data | ||||||
|     try { |     try { | ||||||
|       gpsLayer.isDisplayed.setData(false) |       gpsLayer.isDisplayed.setData(false) | ||||||
|       const geojson: FeatureCollection = downloadHelper.getCleanGeoJson(metaIsIncluded) |  | ||||||
|       const name = state.layout.id |       const name = state.layout.id | ||||||
| 
 | 
 | ||||||
|       const title = `MapComplete_${name}_export_${new Date() |       const title = `MapComplete_${name}_export_${new Date() | ||||||
|         .toISOString() |         .toISOString() | ||||||
|         .substr(0, 19)}.${extension}` |         .substr(0, 19)}.${extension}` | ||||||
|       const promise = construct(geojson, title, status) |       const data: Blob | string = await construct(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 |  | ||||||
|       } |  | ||||||
|       if (!data) { |       if (!data) { | ||||||
|         return |         return | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  | @ -153,7 +153,7 @@ export default class DownloadHelper { | ||||||
|         return header + "\n" + elements.join("\n") + "\n</svg>" |         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 state = this._state | ||||||
|         const featuresPerLayer = new Map<string, any[]>() |         const featuresPerLayer = new Map<string, any[]>() | ||||||
|         const neededLayers = state.layout.layers.filter((l) => l.source !== null).map((l) => l.id) |         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) { |         for (const neededLayer of neededLayers) { | ||||||
|             const indexedFeatureSource = state.perLayer.get(neededLayer) |             const indexedFeatureSource = state.perLayer.get(neededLayer) | ||||||
|  | 
 | ||||||
|             let features = indexedFeatureSource.GetFeaturesWithin(bbox) |             let features = indexedFeatureSource.GetFeaturesWithin(bbox) | ||||||
|             // The 'indexedFeatureSources' contains _all_ features, they are not filtered yet
 |             // The 'indexedFeatureSources' contains _all_ features, they are not filtered yet
 | ||||||
|             const filter = state.layerState.filteredLayers.get(neededLayer) |             const filter = state.layerState.filteredLayers.get(neededLayer) | ||||||
|  |  | ||||||
|  | @ -17,9 +17,16 @@ | ||||||
|   const downloadHelper = new DownloadHelper(state) |   const downloadHelper = new DownloadHelper(state) | ||||||
| 
 | 
 | ||||||
|   let metaIsIncluded = false |   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 maindiv = document.getElementById("maindiv") | ||||||
|     const layers = state.layout.layers.filter((l) => l.source !== null) |     const layers = state.layout.layers.filter((l) => l.source !== null) | ||||||
|     return downloadHelper.asSvg({ |     return downloadHelper.asSvg({ | ||||||
|  | @ -34,6 +41,8 @@ | ||||||
| 
 | 
 | ||||||
| {#if $isLoading} | {#if $isLoading} | ||||||
|   <Loading /> |   <Loading /> | ||||||
|  | {:else if $numberOfFeatures > 100000} | ||||||
|  |   <Tr cls="alert" t={Translations.t.general.download.toMuch} /> | ||||||
| {:else} | {:else} | ||||||
|   <div class="flex w-full flex-col" /> |   <div class="flex w-full flex-col" /> | ||||||
|   <h3> |   <h3> | ||||||
|  | @ -44,20 +53,18 @@ | ||||||
|     {state} |     {state} | ||||||
|     extension="geojson" |     extension="geojson" | ||||||
|     mimetype="application/vnd.geo+json" |     mimetype="application/vnd.geo+json" | ||||||
|     construct={(geojson) => JSON.stringify(geojson)} |     construct={async () => JSON.stringify(await getGeojson())} | ||||||
|     mainText={t.downloadGeojson} |     mainText={t.downloadGeojson} | ||||||
|     helperText={t.downloadGeoJsonHelper} |     helperText={t.downloadGeoJsonHelper} | ||||||
|     {metaIsIncluded} |  | ||||||
|   /> |   /> | ||||||
| 
 | 
 | ||||||
|   <DownloadButton |   <DownloadButton | ||||||
|     {state} |     {state} | ||||||
|     extension="csv" |     extension="csv" | ||||||
|     mimetype="text/csv" |     mimetype="text/csv" | ||||||
|     construct={(geojson) => GeoOperations.toCSV(geojson)} |     construct={async () => GeoOperations.toCSV(await getGeojson())} | ||||||
|     mainText={t.downloadCSV} |     mainText={t.downloadCSV} | ||||||
|     helperText={t.downloadCSVHelper} |     helperText={t.downloadCSVHelper} | ||||||
|     {metaIsIncluded} |  | ||||||
|   /> |   /> | ||||||
| 
 | 
 | ||||||
|   <label class="mb-8 mt-2"> |   <label class="mb-8 mt-2"> | ||||||
|  | @ -67,7 +74,6 @@ | ||||||
| 
 | 
 | ||||||
|   <DownloadButton |   <DownloadButton | ||||||
|     {state} |     {state} | ||||||
|     {metaIsIncluded} |  | ||||||
|     extension="svg" |     extension="svg" | ||||||
|     mimetype="image/svg+xml" |     mimetype="image/svg+xml" | ||||||
|     mainText={t.downloadAsSvg} |     mainText={t.downloadAsSvg} | ||||||
|  | @ -77,7 +83,6 @@ | ||||||
| 
 | 
 | ||||||
|   <DownloadButton |   <DownloadButton | ||||||
|     {state} |     {state} | ||||||
|     {metaIsIncluded} |  | ||||||
|     extension="svg" |     extension="svg" | ||||||
|     mimetype="image/svg+xml" |     mimetype="image/svg+xml" | ||||||
|     mainText={t.downloadAsSvgLinesOnly} |     mainText={t.downloadAsSvgLinesOnly} | ||||||
|  | @ -87,7 +92,6 @@ | ||||||
| 
 | 
 | ||||||
|   <DownloadButton |   <DownloadButton | ||||||
|     {state} |     {state} | ||||||
|     {metaIsIncluded} |  | ||||||
|     extension="png" |     extension="png" | ||||||
|     mimetype="image/png" |     mimetype="image/png" | ||||||
|     mainText={t.downloadAsPng} |     mainText={t.downloadAsPng} | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ | ||||||
|   let t = Translations.t.general.download |   let t = Translations.t.general.download | ||||||
|   const downloadHelper = new DownloadHelper(state) |   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 = | ||||||
|       title.substring(0, title.length - 4) + "_" + template.format + "_" + template.orientation |       title.substring(0, title.length - 4) + "_" + template.format + "_" + template.orientation | ||||||
|     const templateUrls = SvgToPdf.templates[templateName].pages |     const templateUrls = SvgToPdf.templates[templateName].pages | ||||||
|  | @ -33,11 +33,11 @@ | ||||||
|         console.log("Creating an image for key", key) |         console.log("Creating an image for key", key) | ||||||
|         if (key === "qr") { |         if (key === "qr") { | ||||||
|           const toShare = window.location.href.split("#")[0] |           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) |         return downloadHelper.createImage(key, width, height) | ||||||
|       }, |       }, | ||||||
|       textSubstitutions: <Record<string, string>>{ |       textSubstitutions: <Record<string, string | Translation>>{ | ||||||
|         "layout.title": state.layout.title, |         "layout.title": state.layout.title, | ||||||
|         layoutid: state.layout.id, |         layoutid: state.layout.id, | ||||||
|         title: state.layout.title, |         title: state.layout.title, | ||||||
|  | @ -61,7 +61,6 @@ | ||||||
|   construct={constructPdf} |   construct={constructPdf} | ||||||
|   extension="pdf" |   extension="pdf" | ||||||
|   helperText={t.downloadAsPdfHelper} |   helperText={t.downloadAsPdfHelper} | ||||||
|   metaIsIncluded={false} |  | ||||||
|   mainText={t.pdf.current_view_generic.Subs({ |   mainText={t.pdf.current_view_generic.Subs({ | ||||||
|     orientation: template.orientation, |     orientation: template.orientation, | ||||||
|     paper_size: template.format.toUpperCase(), |     paper_size: template.format.toUpperCase(), | ||||||
|  |  | ||||||
|  | @ -20,6 +20,8 @@ import { OsmTags } from "../Models/OsmFeature" | ||||||
| import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource" | import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource" | ||||||
| import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider" | import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider" | ||||||
| import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler" | 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. |  * The state needed to render a special Visualisation. | ||||||
|  | @ -30,12 +32,13 @@ export interface SpecialVisualizationState { | ||||||
|     readonly featureSwitches: FeatureSwitchState |     readonly featureSwitches: FeatureSwitchState | ||||||
| 
 | 
 | ||||||
|     readonly layerState: LayerState |     readonly layerState: LayerState | ||||||
|  |     readonly featureSummary: SummaryTileSourceRewriter | ||||||
|     readonly featureProperties: { |     readonly featureProperties: { | ||||||
|         getStore(id: string): UIEventSource<Record<string, string>> |         getStore(id: string): UIEventSource<Record<string, string>> | ||||||
|         trackFeature?(feature: { properties: OsmTags }) |         trackFeature?(feature: { properties: OsmTags }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     readonly indexedFeatures: IndexedFeatureSource |     readonly indexedFeatures: IndexedFeatureSource & LayoutSource | ||||||
|     /** |     /** | ||||||
|      * Some features will create a new element that should be displayed. |      * Some features will create a new element that should be displayed. | ||||||
|      * These can be injected by appending them to this featuresource (and pinging it) |      * 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 SvelteUIElement from "./Base/SvelteUIElement" | ||||||
| import Filterview from "./BigComponents/Filterview.svelte" | import Filterview from "./BigComponents/Filterview.svelte" | ||||||
| import FilteredLayer from "../Models/FilteredLayer" | import FilteredLayer from "../Models/FilteredLayer" | ||||||
| import DownloadButton from "./DownloadFlow/DownloadButton.svelte" |  | ||||||
| import { SubtleButton } from "./Base/SubtleButton" | import { SubtleButton } from "./Base/SubtleButton" | ||||||
| import { GeoOperations } from "../Logic/GeoOperations" | import { GeoOperations } from "../Logic/GeoOperations" | ||||||
| import { Polygon } from "geojson" | import { Polygon } from "geojson" | ||||||
|  |  | ||||||
|  | @ -553,7 +553,7 @@ class SvgToPdfInternals { | ||||||
| export interface SvgToPdfOptions { | export interface SvgToPdfOptions { | ||||||
|     freeComponentId: string |     freeComponentId: string | ||||||
|     disableMaps?: false | true |     disableMaps?: false | true | ||||||
|     textSubstitutions?: Record<string, string> |     textSubstitutions?: Record<string, string | Translation> | ||||||
|     beforePage?: (i: number) => void |     beforePage?: (i: number) => void | ||||||
|     overrideLocation?: { lat: number; lon: number } |     overrideLocation?: { lat: number; lon: number } | ||||||
|     disableDataLoading?: boolean | false |     disableDataLoading?: boolean | false | ||||||
|  | @ -711,9 +711,13 @@ class SvgToPdfPage { | ||||||
|             this.options.beforePage(i) |             this.options.beforePage(i) | ||||||
|         } |         } | ||||||
|         const self = this |         const self = this | ||||||
|         const internal = new SvgToPdfInternals(advancedApi, this, (key) => |         const internal = new SvgToPdfInternals(advancedApi, this, (key) => { | ||||||
|             self.extractTranslation(key, language) |             const tr = self.extractTranslation(key, language) | ||||||
|         ) |             if (typeof tr === "string") { | ||||||
|  |                 return tr | ||||||
|  |             } | ||||||
|  |             return tr.txt | ||||||
|  |         }) | ||||||
|         for (const child of Array.from(this._svgRoot.children)) { |         for (const child of Array.from(this._svgRoot.children)) { | ||||||
|             internal.handleElement(<any>child) |             internal.handleElement(<any>child) | ||||||
|         } |         } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue