Files
next-tjwater-frontend/src/features/workbench/map/city-scene-control.ts
T

90 lines
2.8 KiB
TypeScript

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:
'<a href="/city-model/ATTRIBUTION.md" target="_blank" rel="noopener noreferrer">Qian Shi et al. · 数据来源与许可</a>'
});
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();
};
}