feat: extend supply workbench demo and map navigation

This commit is contained in:
2026-09-23 13:19:30 +08:00
parent bebcf2a06c
commit 1752b9ef84
45 changed files with 2137 additions and 449 deletions
@@ -50,6 +50,7 @@ import {
scadaInteractionLayers
} from "../map/scada";
import { registerSupplyAssetImages } from "../map/supply-icons";
import { installCityScene } from "../map/city-scene-control";
import { useMapInteractions } from "./use-map-interactions";
import { useSimulationLayerVisibility } from "./use-simulation-layer-visibility";
@@ -79,7 +80,10 @@ type UseWorkbenchMapResult = {
sourceStatuses: WorkbenchSourceStatus[];
availableSourceIds: WaterNetworkSourceId[];
sourceFeatureCounts: Partial<Record<WaterNetworkSourceId, number>>;
getScadaFeatures: (deviceIds?: string[], signal?: AbortSignal) => Promise<FeatureCollection<Point, ScadaDevice>>;
getScadaFeatures: (
deviceIds?: string[],
signal?: AbortSignal
) => Promise<FeatureCollection<Point, ScadaDevice>>;
scadaObservedAt: string | null;
refreshGeoServerLayers: () => boolean;
fitNetworkBounds: () => void;
@@ -96,6 +100,7 @@ export type WorkbenchSourceStatus = {
const SOURCE_STATUS_LABELS: Record<string, string> = {
"mapbox-base": "Mapbox 底图",
"tianditu-base": "天地图底图",
"geoserver-mvt": "GeoServer MVT",
"geoserver-catalog": "GeoServer 图层目录",
"scada-api": "SCADA API",
@@ -106,6 +111,10 @@ const SOURCE_STATUS_LABELS: Record<string, string> = {
const SOURCE_GROUP_BY_ID: Record<string, string> = {
"mapbox-light": "mapbox-base",
"mapbox-satellite": "mapbox-base",
"tianditu-vector": "tianditu-base",
"tianditu-vector-annotation": "tianditu-base",
"tianditu-image": "tianditu-base",
"tianditu-image-annotation": "tianditu-base",
...Object.fromEntries(
GEOSERVER_WATER_NETWORK_SOURCE_IDS.map((sourceId) => [sourceId, "geoserver-mvt"])
),
@@ -197,9 +206,11 @@ export function useWorkbenchMap({
? current
: availability.availableSourceIds
);
setSourceFeatureCounts((current) => areFeatureCountsEqual(current, availability.featureCounts)
? current
: availability.featureCounts);
setSourceFeatureCounts((current) =>
areFeatureCountsEqual(current, availability.featureCounts)
? current
: availability.featureCounts
);
if (availability.failedSourceIds.length > 0) {
updateSourceStatus(
@@ -251,12 +262,13 @@ export function useWorkbenchMap({
setMapReady(false);
setMapError(null);
const mapboxToken = env.TJWATER_MAPBOX_ACCESS_TOKEN || undefined;
const tiandituToken = env.TJWATER_TIANDITU_TOKEN || undefined;
const scadaAbortController = new AbortController();
let scadaRefreshTimeoutId: number | undefined;
updateSourceStatus(setSourceStatuses, "scada-api", "loading", "正在通过API加载SCADA设备。");
const map = new maplibregl.Map({
container: containerRef.current,
style: createBaseStyle(mapboxToken),
style: createBaseStyle(mapboxToken, tiandituToken),
pitch: 0,
bearing: 0,
maxZoom: MAP_MAX_ZOOM,
@@ -265,9 +277,11 @@ export function useWorkbenchMap({
});
window.__waterNetworkMap = map;
let removeCityScene: (() => void) | undefined;
map.on("load", async () => {
map.resize();
removeCityScene = installCityScene(map);
(map.getSource(SCADA_SOURCE_ID) as GeoJSONSource | undefined)?.setData(
scadaCollectionRef.current
);
@@ -279,11 +293,7 @@ export function useWorkbenchMap({
map.addSource(SIMULATION_SOURCE_IDS.impactArea, simulationSources.impactArea);
map.addSource(SIMULATION_SOURCE_IDS.annotations, simulationSources.annotations);
map.addSource(SIMULATION_SOURCE_IDS.backendAnalysis, simulationSources.backendAnalysis);
syncSimulationSourceData(
map,
simulationOverlayRef.current,
allowDemoSimulationRef.current
);
syncSimulationSourceData(map, simulationOverlayRef.current, allowDemoSimulationRef.current);
await registerSupplyAssetImages(map);
const scadaRegistration = await registerScadaImages(map);
const activeScadaBusinessLayers =
@@ -340,10 +350,7 @@ export function useWorkbenchMap({
});
const refreshScada = async () => {
const snapshotPromise = fetchScadaSnapshot(
getAccessToken,
scadaAbortController.signal
);
const snapshotPromise = fetchScadaSnapshot(getAccessToken, scadaAbortController.signal);
scadaLoadPromiseRef.current = snapshotPromise
.then((snapshot) => snapshot.collection)
.catch(() => scadaCollectionRef.current);
@@ -428,6 +435,7 @@ export function useWorkbenchMap({
if (scadaRefreshTimeoutId !== undefined) {
window.clearTimeout(scadaRefreshTimeoutId);
}
removeCityScene?.();
map.remove();
mapRef.current = null;
delete window.__waterNetworkMap;
@@ -500,8 +508,10 @@ function areFeatureCountsEqual(
) {
const currentEntries = Object.entries(current);
const nextEntries = Object.entries(next);
return currentEntries.length === nextEntries.length
&& nextEntries.every(([sourceId, count]) => current[sourceId as WaterNetworkSourceId] === count);
return (
currentEntries.length === nextEntries.length &&
nextEntries.every(([sourceId, count]) => current[sourceId as WaterNetworkSourceId] === count)
);
}
function networkViewKey(view: NetworkViewParams) {
@@ -549,8 +559,9 @@ function syncDataDependentGeoServerLayers(
DATA_DEPENDENT_GEOSERVER_SOURCE_IDS.forEach((sourceId) => {
if (availableSourceIdSet.has(sourceId)) return;
map.getStyle().layers
.filter((layer) => "source" in layer && layer.source === sourceId)
map
.getStyle()
.layers.filter((layer) => "source" in layer && layer.source === sourceId)
.reverse()
.forEach((layer) => map.removeLayer(layer.id));
if (map.getSource(sourceId)) map.removeSource(sourceId);
@@ -711,11 +722,15 @@ function getSourceGroupFromErrorEvent(event: { sourceId?: string; error?: { mess
return "mapbox-base";
}
if (message.includes("tianditu.gov.cn") || message.includes("tianditu")) {
return "tianditu-base";
}
return null;
}
function getSourceStatusMessage(sourceGroupId: string, status: WorkbenchSourceStatusValue) {
if (sourceGroupId === "mapbox-base") {
if (["mapbox-base", "tianditu-base"].includes(sourceGroupId)) {
return status === "online"
? "底图瓦片已恢复正常。"
: "底图瓦片请求中断,业务图层仍可继续使用。";