import {Utils} from "../../../Utils"; import * as OsmToGeoJson from "osmtogeojson"; import StaticFeatureSource from "../Sources/StaticFeatureSource"; import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter"; import {Store, UIEventSource} from "../../UIEventSource"; import FilteredLayer from "../../../Models/FilteredLayer"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {Tiles} from "../../../Models/TileRange"; import {BBox} from "../../BBox"; import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; import {Or} from "../../Tags/Or"; import {TagsFilter} from "../../Tags/TagsFilter"; import {OsmObject} from "../../Osm/OsmObject"; /** * If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile' */ export default class OsmFeatureSource { public readonly isRunning: UIEventSource = new UIEventSource(false) public readonly downloadedTiles = new Set() public rawDataHandlers: ((osmJson: any, tileId: number) => void)[] = [] private readonly _backend: string; private readonly filteredLayers: Store; private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void; private isActive: Store; private options: { handleTile: (tile: FeatureSourceForLayer & Tiled) => void; isActive: Store, neededTiles: Store, markTileVisited?: (tileId: number) => void }; private readonly allowedTags: TagsFilter; /** * * @param options: allowedFeatures is normally calculated from the layoutToUse */ constructor(options: { handleTile: (tile: FeatureSourceForLayer & Tiled) => void; isActive: Store, neededTiles: Store, state: { readonly filteredLayers: UIEventSource; readonly osmConnection: { Backend(): string }; readonly layoutToUse?: LayoutConfig }, readonly allowedFeatures?: TagsFilter, markTileVisited?: (tileId: number) => void }) { this.options = options; this._backend = options.state.osmConnection.Backend(); this.filteredLayers = options.state.filteredLayers.map(layers => layers.filter(layer => layer.layerDef.source.geojsonSource === undefined)) this.handleTile = options.handleTile this.isActive = options.isActive const self = this options.neededTiles.addCallbackAndRunD(neededTiles => { self.Update(neededTiles) }) const neededLayers = (options.state.layoutToUse?.layers ?? []) .filter(layer => !layer.doNotDownload) .filter(layer => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer) this.allowedTags = options.allowedFeatures ?? new Or(neededLayers.map(l => l.source.osmTags)) } private async Update(neededTiles: number[]) { if (this.options.isActive?.data === false) { return; } neededTiles = neededTiles.filter(tile => !this.downloadedTiles.has(tile)) if (neededTiles.length == 0) { return; } this.isRunning.setData(true) try { for (const neededTile of neededTiles) { this.downloadedTiles.add(neededTile) await this.LoadTile(...Tiles.tile_from_index(neededTile)) } } catch (e) { console.error(e) } finally { this.isRunning.setData(false) } } /** * The requested tile might only contain part of the relation. * * This method will download the full relation and return it as geojson if it was incomplete. * If the feature is already complete (or is not a relation), the feature will be returned */ private async patchIncompleteRelations(feature: {properties: {id: string}}, originalJson: {elements: {type: "node" | "way" | "relation", id: number, } []}): Promise { if(!feature.properties.id.startsWith("relation")){ return feature } const relationSpec = originalJson.elements.find(f => "relation/"+f.id === feature.properties.id) const members : {type: string, ref: number}[] = relationSpec["members"] for (const member of members) { const isFound = originalJson.elements.some(f => f.id === member.ref && f.type === member.type) if (isFound) { continue } // This member is missing. We redownload the entire relation instead console.debug("Fetching incomplete relation "+feature.properties.id) return (await OsmObject.DownloadObjectAsync(feature.properties.id)).asGeoJson() } return feature; } private async LoadTile(z, x, y): Promise { if (z >= 22) { throw "This is an absurd high zoom level" } if (z < 14) { throw `Zoom ${z} is too much for OSM to handle! Use a higher zoom level!` } const bbox = BBox.fromTile(z, x, y) const url = `${this._backend}/api/0.6/map?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}` let error = undefined; try { const osmJson = await Utils.downloadJson(url) try { console.log("Got tile", z, x, y, "from the osm api") this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y))) const geojson = OsmToGeoJson.default(osmJson, // @ts-ignore { flatProperties: true }); // The geojson contains _all_ features at the given location // We only keep what is needed geojson.features = geojson.features.filter(feature => this.allowedTags.matchesProperties(feature.properties)) for (let i = 0; i < geojson.features.length; i++) { geojson.features[i] = await this.patchIncompleteRelations(geojson.features[i], osmJson) } geojson.features.forEach(f => { f.properties["_backend"] = this._backend }) const index = Tiles.tile_index(z, x, y); new PerLayerFeatureSourceSplitter(this.filteredLayers, this.handleTile, StaticFeatureSource.fromGeojson(geojson.features), { tileIndex: index } ); if (this.options.markTileVisited) { this.options.markTileVisited(index) } }catch(e){ console.error("PANIC: got the tile from the OSM-api, but something crashed handling this tile") error = e; } } catch (e) { console.error("Could not download tile", z, x, y, "due to", e, "; retrying with smaller bounds") if (e === "rate limited") { return; } await this.LoadTile(z + 1, x * 2, y * 2) await this.LoadTile(z + 1, 1 + x * 2, y * 2) await this.LoadTile(z + 1, x * 2, 1 + y * 2) await this.LoadTile(z + 1, 1 + x * 2, 1 + y * 2) } if(error !== undefined){ throw error; } } }