90 lines
2.8 KiB
TypeScript
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();
|
|
};
|
|
}
|