import {Utils} from "../../../Utils"; import * as OsmToGeoJson from "osmtogeojson"; import StaticFeatureSource from "../Sources/StaticFeatureSource"; import PerLayerFeatureSourceSplitter from "../PerLayerFeatureSourceSplitter"; import {UIEventSource} from "../../UIEventSource"; import FilteredLayer from "../../../Models/FilteredLayer"; import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {Tiles} from "../../../Models/TileRange"; import {BBox} from "../../BBox"; import {OsmConnection} from "../../Osm/OsmConnection"; import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; import {Or} from "../../Tags/Or"; import {TagsFilter} from "../../Tags/TagsFilter"; /** * 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: UIEventSource; private readonly handleTile: (fs: (FeatureSourceForLayer & Tiled)) => void; private isActive: UIEventSource; private options: { handleTile: (tile: FeatureSourceForLayer & Tiled) => void; isActive: UIEventSource, neededTiles: UIEventSource, state: { readonly osmConnection: OsmConnection; }, markTileVisited?: (tileId: number) => void }; private readonly allowedTags: TagsFilter; constructor(options: { handleTile: (tile: FeatureSourceForLayer & Tiled) => void; isActive: UIEventSource, neededTiles: UIEventSource, state: { readonly filteredLayers: UIEventSource; readonly osmConnection: OsmConnection; readonly layoutToUse: LayoutConfig }, markTileVisited?: (tileId: number) => void }) { this.options = options; this._backend = options.state.osmConnection._oauth_config.url; 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 => { if (options.isActive?.data === false) { return; } neededTiles = neededTiles.filter(tile => !self.downloadedTiles.has(tile)) if (neededTiles.length == 0) { return; } self.isRunning.setData(true) try { for (const neededTile of neededTiles) { self.downloadedTiles.add(neededTile) self.LoadTile(...Tiles.tile_from_index(neededTile)).then(_ => { console.debug("Tile ", Tiles.tile_from_index(neededTile).join("/"), "loaded from OSM") }) } } catch (e) { console.error(e) } finally { self.isRunning.setData(false) } }) const neededLayers = (options.state.layoutToUse?.layers ?? []) .filter(layer => !layer.doNotDownload) .filter(layer => layer.source.geojsonSource === undefined || layer.source.isOsmCacheLayer) this.allowedTags = new Or(neededLayers.map(l => l.source.osmTags)) } private async LoadTile(z, x, y): Promise { if (z > 25) { 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}` try { const osmJson = await Utils.downloadJson(url) try { console.debug("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)) geojson.features.forEach(f => { f.properties["_backend"] = this._backend }) const index = Tiles.tile_index(z, x, y); new PerLayerFeatureSourceSplitter(this.filteredLayers, this.handleTile, new StaticFeatureSource(geojson.features, false), { tileIndex: index } ); if (this.options.markTileVisited) { this.options.markTileVisited(index) } } catch (e) { console.error("Weird 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) return; } } }