UNPKG

maplibre-gl-indoor

Version:

A MapLibre plugin to visualize multi-level buildings

150 lines (126 loc) 4.71 kB
import type { BBox } from "geojson"; import type { Map } from "maplibre-gl"; import { default as turfDestination } from "@turf/destination"; import { default as turfDistance } from "@turf/distance"; import type { IndoorMapOptions, MapGLWithIndoor } from "./Types"; import addIndoorTo from "./addIndoorTo"; import { bboxContains } from "./bbox"; import IndoorMap from "./IndoorMap"; type RemoteMap = { indoorMap?: IndoorMap; name: string; url: string; }; const MIN_ZOOM_TO_DOWNLOAD = 16.25; const MIN_DISTANCE_TO_DOWNLOAD_METERS = 200; class MapServerHandler { downloadedBounds: BBox | null; indoorMapOptions?: IndoorMapOptions; loadMapsPromise: Promise<void> = Promise.resolve(); map: MapGLWithIndoor; remoteMapsDownloaded: RemoteMap[]; serverUrl: string; private constructor( serverUrl: string, map: MapGLWithIndoor, indoorMapOptions?: IndoorMapOptions, ) { this.serverUrl = serverUrl; this.map = map; this.indoorMapOptions = indoorMapOptions; this.remoteMapsDownloaded = []; this.downloadedBounds = null; if (map.loaded()) { this.loadMapsIfNecessary(); } else { map.on("load", () => this.loadMapsIfNecessary()); } map.on("move", () => this.loadMapsIfNecessary()); } static manage(server: string, map: Map, indoorMapOptions?: IndoorMapOptions) { return new MapServerHandler(server, addIndoorTo(map), indoorMapOptions); } private async addCustomMap(map: RemoteMap) { const geojson = await (await fetch(map.url)).json(); map.indoorMap = IndoorMap.fromGeojson(geojson, this.indoorMapOptions); await this.map.indoor.addMap(map.indoorMap); this.remoteMapsDownloaded.push(map); } private async loadMapsIfNecessary() { if (this.map.getZoom() < MIN_ZOOM_TO_DOWNLOAD) { return; } const viewPort = this.map.getBounds(); if (this.downloadedBounds !== null) { if ( bboxContains( this.downloadedBounds, viewPort.getNorthEast().toArray(), ) && bboxContains(this.downloadedBounds, viewPort.getSouthWest().toArray()) ) { // Maps of the viewport have already been downloaded. return; } } const distanceEastWestMeters = turfDistance( viewPort.getNorthEast().toArray(), viewPort.getNorthWest().toArray(), { units: "meters" }, ); const distanceNorthSouthMeters = turfDistance( viewPort.getNorthEast().toArray(), viewPort.getSouthEast().toArray(), { units: "meters" }, ); // It is not necessary to compute others as we are at zoom >= 16.25, the approximation is enough. const maxXYViewportDistanceMeters = Math.max(distanceEastWestMeters, distanceNorthSouthMeters) / 2; const verticalDistanceMeters = Math.max( MIN_DISTANCE_TO_DOWNLOAD_METERS, maxXYViewportDistanceMeters * 2, ); const center = this.map.getCenter().toArray(); console.debug( `requested indoor maps for a bbox ${verticalDistanceMeters}m vertically/horizontally around ${center}`, ); // isosceles right triangle => diagonal is x * sqrt(2) const distDiagonalMeters = verticalDistanceMeters * Math.sqrt(2); const northEast = turfDestination(center, distDiagonalMeters, 50, { units: "meters", }); const southWest = turfDestination(center, distDiagonalMeters, 50 - 180, { units: "meters", }); const boundsToDownload = [ ...southWest.geometry.coordinates.reverse(), ...northEast.geometry.coordinates.reverse(), ] as BBox; // TODO: I put this here because fetch is async and takes more time than the next call to loadMapsIfNecessary. this.downloadedBounds = boundsToDownload; await this.loadMapsPromise; this.loadMapsPromise = this.loadMapsInBounds(boundsToDownload); } private async loadMapsInBounds(bounds: BBox) { const url = `${this.serverUrl}?bbox=${bounds[0]},${bounds[1]},${bounds[2]},${bounds[3]}`; const maps: RemoteMap[] = await (await fetch(url)).json(); const mapsToRemove: RemoteMap[] = []; const mapsToAdd: RemoteMap[] = []; for (const map of maps) { if (!maps.find((_map) => _map.url === map.url)) { mapsToRemove.push(map); } else if ( !this.remoteMapsDownloaded.find((_map) => _map.url === map.url) ) { mapsToAdd.push(map); } } mapsToAdd.forEach(this.addCustomMap.bind(this)); mapsToRemove.forEach(this.removeCustomMap.bind(this)); } private async removeCustomMap(map: RemoteMap) { await this.map.indoor.removeMap(map.indoorMap!); this.remoteMapsDownloaded.splice(this.remoteMapsDownloaded.indexOf(map), 1); } } export default MapServerHandler;