feat: extend supply workbench demo and map navigation
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import maplibregl, { type Map as MapLibreMap } from "maplibre-gl";
|
||||
import {
|
||||
CITY_LAYERS,
|
||||
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;
|
||||
Object.entries(citySources()).forEach(([id, source]) => map.addSource(id, source));
|
||||
const firstBusinessLayer = findCitySceneInsertionLayerId(map.getStyle().layers);
|
||||
CITY_LAYERS.forEach((layer) => map.addLayer(layer, firstBusinessLayer));
|
||||
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();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user