import maplibregl, { type Map as MapLibreMap } from "maplibre-gl"; import { CITY_LAYERS, CITY_LIGHT, CITY_ROOT, cityManifestSchema, citySources, findCitySceneInsertionLayerId } from "./city-scene-layers"; import { Protocol } from "pmtiles"; const cityTileProtocol = new Protocol(); maplibregl.addProtocol("pmtiles", cityTileProtocol.tile); class CitySceneControl { private map?: MapLibreMap; private initialized = false; private attribution?: maplibregl.AttributionControl; attach(map: MapLibreMap) { this.map = map; map.on("pitchend", this.syncVisibility); this.syncVisibility(); } private syncVisibility = () => { const map = this.map; if (!map) return; const enabled = map.getPitch() > 0; if (enabled && !this.initialized) this.addLayers(); if (!this.initialized) return; CITY_LAYERS.forEach(({ id }) => map.setLayoutProperty(id, "visibility", enabled ? "visible" : "none") ); }; private addLayers() { const map = this.map; if (!map) return; map.setLight(CITY_LIGHT); Object.entries(citySources()).forEach(([id, source]) => map.addSource(id, source)); const firstHitLayer = findCitySceneInsertionLayerId(map.getStyle().layers); CITY_LAYERS.forEach((layer) => map.addLayer(layer, firstHitLayer)); this.attribution = new maplibregl.AttributionControl({ compact: true, customAttribution: 'Qian Shi et al. · 数据来源与许可' }); map.addControl(this.attribution, "bottom-left"); this.initialized = true; } onRemove() { const map = this.map; map?.off("pitchend", this.syncVisibility); if (this.attribution && map?.hasControl(this.attribution)) map.removeControl(this.attribution); if (map && this.initialized) { [...CITY_LAYERS].reverse().forEach(({ id }) => { if (map.getLayer(id)) map.removeLayer(id); }); Object.keys(citySources()).forEach((id) => { if (map.getSource(id)) map.removeSource(id); }); } this.map = undefined; } } /** Scene is optional; absent local data must not break the operational map. */ export function installCityScene(map: MapLibreMap): () => void { const abort = new AbortController(); let control: CitySceneControl | undefined; void fetch(`${CITY_ROOT}/manifest.json`, { signal: abort.signal }) .then(async (response) => { if (!response.ok) return; const manifest = cityManifestSchema.safeParse(await response.json()); if (!manifest.success || abort.signal.aborted) return; control = new CitySceneControl(); control.attach(map); }) .catch(() => { /* Optional scene is not deployed on every installation. */ }); return () => { abort.abort(); control?.onRemove(); }; }