diff --git a/src/app/(main)/hydraulic-simulation/burst-simulation/page.tsx b/src/app/(main)/hydraulic-simulation/burst-simulation/page.tsx
index 8078cf7..86f7864 100644
--- a/src/app/(main)/hydraulic-simulation/burst-simulation/page.tsx
+++ b/src/app/(main)/hydraulic-simulation/burst-simulation/page.tsx
@@ -8,7 +8,11 @@ export default function Home() {
return (
-
+
diff --git a/src/app/(main)/hydraulic-simulation/contaminant-simulation/page.tsx b/src/app/(main)/hydraulic-simulation/contaminant-simulation/page.tsx
index 2c3d499..265c631 100644
--- a/src/app/(main)/hydraulic-simulation/contaminant-simulation/page.tsx
+++ b/src/app/(main)/hydraulic-simulation/contaminant-simulation/page.tsx
@@ -8,7 +8,11 @@ export default function Home() {
return (
-
+
diff --git a/src/components/olmap/core/Controls/BaseLayers.tsx b/src/components/olmap/core/Controls/BaseLayers.tsx
index 7a6c09a..aa43007 100644
--- a/src/components/olmap/core/Controls/BaseLayers.tsx
+++ b/src/components/olmap/core/Controls/BaseLayers.tsx
@@ -1,158 +1,174 @@
-import React, { useState, useEffect } from "react";
+"use client";
+
+import React, { useState, useEffect, useMemo, useRef } from "react";
import Image from "next/image";
-import { useMap } from "../MapComponent";
+import { useData, useMap } from "../MapComponent";
import TileLayer from "ol/layer/Tile.js";
import XYZ from "ol/source/XYZ.js";
+import Group from "ol/layer/Group";
import mapboxOutdoors from "@assets/map/layers/mapbox-outdoors.png";
import mapboxLight from "@assets/map/layers/mapbox-light.png";
import mapboxSatellite from "@assets/map/layers/mapbox-satellite.png";
import mapboxSatelliteStreet from "@assets/map/layers/mapbox-satellite-streets.png";
import mapboxStreets from "@assets/map/layers/mapbox-streets.png";
import clsx from "clsx";
-import Group from "ol/layer/Group";
-import { MAPBOX_TOKEN } from "@config/config";
-import { TIANDITU_TOKEN } from "@config/config";
+import { MAPBOX_TOKEN, TIANDITU_TOKEN } from "@config/config";
+import type { Map as OlMap } from "ol";
+
const INITIAL_LAYER = "mapbox-light";
-const streetsLayer = new TileLayer({
- source: new XYZ({
- url: `https://api.mapbox.com/styles/v1/mapbox/streets-v12/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
- tileSize: 512,
- maxZoom: 20,
- projection: "EPSG:3857",
- attributions:
- '数据来源:Mapbox & OpenStreetMap',
- }),
-});
-const lightMapLayer = new TileLayer({
- source: new XYZ({
- url: `https://api.mapbox.com/styles/v1/mapbox/light-v11/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
- tileSize: 512,
- maxZoom: 20,
- projection: "EPSG:3857",
- attributions:
- '数据来源:Mapbox & OpenStreetMap',
- }),
-});
-const satelliteLayer = new TileLayer({
- source: new XYZ({
- url: `https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
- tileSize: 512,
- maxZoom: 20,
- projection: "EPSG:3857",
- attributions:
- '数据来源:Mapbox & OpenStreetMap',
- }),
-});
-const satelliteStreetsLayer = new TileLayer({
- source: new XYZ({
- url: `https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
- tileSize: 512,
- maxZoom: 20,
- projection: "EPSG:3857",
- attributions:
- '数据来源:Mapbox & OpenStreetMap',
- }),
-});
+const createTileLayer = (url: string, attributions: string) =>
+ new TileLayer({
+ source: new XYZ({
+ url,
+ tileSize: 512,
+ maxZoom: 20,
+ projection: "EPSG:3857",
+ attributions,
+ }),
+ });
-const tiandituVectorLayer = new TileLayer({
- source: new XYZ({
- url: `https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
- projection: "EPSG:3857",
- attributions: '数据来源:天地图',
- }),
-});
-const tiandituVectorAnnotationLayer = new TileLayer({
- source: new XYZ({
- url: `https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
- projection: "EPSG:3857",
- attributions: '数据来源:天地图',
- }),
-});
-const tiandituImageLayer = new TileLayer({
- source: new XYZ({
- url: `https://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
- projection: "EPSG:3857",
- attributions: '数据来源:天地图',
- }),
-});
-const tiandituImageAnnotationLayer = new TileLayer({
- source: new XYZ({
- url: `https://t0.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
- projection: "EPSG:3857",
- attributions: '数据来源:天地图',
- }),
-});
-const tiandituVectorLayerGroup = new Group({
- layers: [tiandituVectorLayer, tiandituVectorAnnotationLayer],
-});
-const tiandituImageLayerGroup = new Group({
- layers: [tiandituImageLayer, tiandituImageAnnotationLayer],
-});
-const baseLayers = [
- {
- id: "mapbox-light",
- name: "默认地图",
- layer: lightMapLayer,
- // layer: tiandituVectorLayerGroup,
- img: mapboxLight.src,
- },
- {
- id: "mapbox-satellite",
- name: "卫星地图",
- layer: satelliteLayer,
- // layer: tiandituImageLayerGroup,
- img: mapboxSatellite.src,
- },
- {
- id: "mapbox-satellite-streets",
- name: "卫星街道地图",
- layer: satelliteStreetsLayer,
- img: mapboxSatelliteStreet.src,
- },
- {
- id: "mapbox-streets",
- name: "街道地图",
- layer: streetsLayer,
- img: mapboxStreets.src,
- },
-];
+const createBaseLayerEntries = () => {
+ const streetsLayer = createTileLayer(
+ `https://api.mapbox.com/styles/v1/mapbox/streets-v12/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
+ '数据来源:Mapbox & OpenStreetMap'
+ );
+ const lightMapLayer = createTileLayer(
+ `https://api.mapbox.com/styles/v1/mapbox/light-v11/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
+ '数据来源:Mapbox & OpenStreetMap'
+ );
+ const satelliteLayer = createTileLayer(
+ `https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
+ '数据来源:Mapbox & OpenStreetMap'
+ );
+ const satelliteStreetsLayer = createTileLayer(
+ `https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
+ '数据来源:Mapbox & OpenStreetMap'
+ );
+
+ const tiandituVectorLayer = new TileLayer({
+ source: new XYZ({
+ url: `https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
+ projection: "EPSG:3857",
+ attributions: '数据来源:天地图',
+ }),
+ });
+ const tiandituVectorAnnotationLayer = new TileLayer({
+ source: new XYZ({
+ url: `https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
+ projection: "EPSG:3857",
+ attributions: '数据来源:天地图',
+ }),
+ });
+ const tiandituImageLayer = new TileLayer({
+ source: new XYZ({
+ url: `https://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
+ projection: "EPSG:3857",
+ attributions: '数据来源:天地图',
+ }),
+ });
+ const tiandituImageAnnotationLayer = new TileLayer({
+ source: new XYZ({
+ url: `https://t0.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_TOKEN}`,
+ projection: "EPSG:3857",
+ attributions: '数据来源:天地图',
+ }),
+ });
+
+ return [
+ {
+ id: "mapbox-light",
+ name: "默认地图",
+ layer: lightMapLayer,
+ img: mapboxLight.src,
+ },
+ {
+ id: "mapbox-satellite",
+ name: "卫星地图",
+ layer: satelliteLayer,
+ img: mapboxSatellite.src,
+ },
+ {
+ id: "mapbox-satellite-streets",
+ name: "卫星街道地图",
+ layer: satelliteStreetsLayer,
+ img: mapboxSatelliteStreet.src,
+ },
+ {
+ id: "mapbox-streets",
+ name: "街道地图",
+ layer: streetsLayer,
+ img: mapboxStreets.src,
+ },
+ {
+ id: "tianditu-vector",
+ name: "天地图矢量",
+ layer: new Group({
+ layers: [tiandituVectorLayer, tiandituVectorAnnotationLayer],
+ }),
+ img: mapboxOutdoors.src,
+ },
+ {
+ id: "tianditu-image",
+ name: "天地图影像",
+ layer: new Group({
+ layers: [tiandituImageLayer, tiandituImageAnnotationLayer],
+ }),
+ img: mapboxSatellite.src,
+ },
+ ];
+};
const BaseLayers: React.FC = () => {
const map = useMap();
- // 切换底图选项展开,控制显示和卸载
+ const data = useData();
+ const maps = useMemo(() => {
+ if (data?.maps?.length) return data.maps;
+ return map ? [map] : [];
+ }, [data?.maps, map]);
+ const layerSetsRef = useRef(new WeakMap>());
const [isShow, setShow] = useState(false);
const [isExpanded, setExpanded] = useState(false);
- // 快速切换底图
const [activeId, setActiveId] = useState(INITIAL_LAYER);
- // 初始化默认底图
useEffect(() => {
- if (!map) return;
- // 添加所有底图至地图并根据 activeId 控制可见性
- baseLayers.forEach((layerInfo) => {
- const layers = map.getLayers().getArray();
- if (!layers.includes(layerInfo.layer)) {
- map.getLayers().insertAt(0, layerInfo.layer);
+ maps.forEach((targetMap) => {
+ let layerEntries = layerSetsRef.current.get(targetMap);
+ if (!layerEntries) {
+ layerEntries = createBaseLayerEntries();
+ layerSetsRef.current.set(targetMap, layerEntries);
}
- layerInfo.layer.setVisible(layerInfo.id === activeId);
+
+ layerEntries.forEach((layerInfo) => {
+ const layers = targetMap.getLayers().getArray();
+ if (!layers.includes(layerInfo.layer)) {
+ targetMap.getLayers().insertAt(0, layerInfo.layer);
+ }
+ layerInfo.layer.setVisible(layerInfo.id === activeId);
+ });
});
- }, [map, activeId]);
+ }, [activeId, maps]);
const changeMapLayers = (id: string) => {
- if (map) {
- // 根据 id 设置每个图层的可见性
- baseLayers.forEach(({ id: lid, layer }) => {
- layer.setVisible(lid === id);
+ maps.forEach((targetMap) => {
+ const layerEntries = layerSetsRef.current.get(targetMap);
+ layerEntries?.forEach(({ id: layerId, layer }) => {
+ layer.setVisible(layerId === id);
});
- }
+ });
};
+ const baseLayers = useMemo(() => createBaseLayerEntries().map(({ id, name, img }) => ({
+ id,
+ name,
+ img,
+ })), []);
+
const handleQuickSwitch = () => {
const nextId =
activeId === baseLayers[0].id ? baseLayers[1].id : baseLayers[0].id;
setActiveId(nextId);
- handleMapLayers(nextId);
+ changeMapLayers(nextId);
};
const handleMapLayers = (id: string) => {
@@ -160,7 +176,6 @@ const BaseLayers: React.FC = () => {
changeMapLayers(id);
};
- // 记录定时器,避免多次触发
const hideTimer = React.useRef(null);
const handleEnter = () => {
@@ -217,7 +232,7 @@ const BaseLayers: React.FC = () => {
{isExpanded && (
{
{baseLayers.map((item) => (
{showPropertyPanel && }
{showDrawPanel && map && }
diff --git a/src/components/olmap/core/MapComponent.tsx b/src/components/olmap/core/MapComponent.tsx
index 2fa0abb..546ac49 100644
--- a/src/components/olmap/core/MapComponent.tsx
+++ b/src/components/olmap/core/MapComponent.tsx
@@ -7,6 +7,7 @@ import React, {
useState,
useEffect,
useMemo,
+ useCallback,
useRef,
} from "react";
import { Map as OlMap, VectorTile } from "ol";
@@ -49,6 +50,13 @@ interface DataContextType {
setCurrentJunctionCalData?: React.Dispatch>;
currentPipeCalData?: any[]; // 当前计算结果
setCurrentPipeCalData?: React.Dispatch>;
+ compareJunctionCalData?: any[];
+ setCompareJunctionCalData?: React.Dispatch>;
+ comparePipeCalData?: any[];
+ setComparePipeCalData?: React.Dispatch>;
+ isCompareMode?: boolean;
+ setCompareMode?: React.Dispatch>;
+ toggleCompareMode?: () => void;
showJunctionText?: boolean; // 是否显示节点文本
showPipeText?: boolean; // 是否显示管道文本
showJunctionId?: boolean; // 是否显示节点ID
@@ -69,6 +77,10 @@ interface DataContextType {
setPipeText?: React.Dispatch>;
setContours?: React.Dispatch>;
deckLayer?: DeckLayer;
+ compareDeckLayer?: DeckLayer;
+ deckLayers?: DeckLayer[];
+ compareMap?: OlMap;
+ maps?: OlMap[];
diameterRange?: [number, number];
elevationRange?: [number, number];
}
@@ -128,12 +140,18 @@ const MapComponent: React.FC = ({ children }) => {
const mapRef = useRef(null);
const canvasRef = useRef(null);
+ const compareMapRef = useRef(null);
+ const compareCanvasRef = useRef(null);
const deckLayerRef = useRef(null);
+ const compareDeckLayerRef = useRef(null);
const isDisposingRef = useRef(false);
+ const isCompareDisposingRef = useRef(false);
const pendingTimeoutsRef = useRef([]);
const [map, setMap] = useState();
const [deckLayer, setDeckLayer] = useState();
+ const [compareMap, setCompareMap] = useState();
+ const [compareDeckLayer, setCompareDeckLayer] = useState();
// currentCalData 用于存储当前计算结果
const [currentTime, setCurrentTime] = useState(-1); // 默认选择当前时间
// const [selectedDate, setSelectedDate] = useState(new Date("2025-9-17"));
@@ -144,6 +162,11 @@ const MapComponent: React.FC = ({ children }) => {
[],
);
const [currentPipeCalData, setCurrentPipeCalData] = useState([]);
+ const [compareJunctionCalData, setCompareJunctionCalData] = useState(
+ [],
+ );
+ const [comparePipeCalData, setComparePipeCalData] = useState([]);
+ const [isCompareMode, setCompareMode] = useState(false);
// junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染
// currentJunctionCalData 和 currentPipeCalData 变化时会新增并更新 junctionData 和 pipeData 的计算属性值
const [junctionData, setJunctionDataState] = useState([]);
@@ -201,6 +224,37 @@ const MapComponent: React.FC = ({ children }) => {
});
}, [pipeData, currentPipeCalData, pipeText]);
+ const mergedCompareJunctionData = useMemo(() => {
+ const nodeMap = new Map(compareJunctionCalData.map((r: any) => [r.ID, r]));
+ return junctionData.map((j) => {
+ const record = nodeMap.get(j.id);
+ let val = record ? record.value : undefined;
+ if (val !== undefined && junctionText === "actualdemand") {
+ val = toM3h(val, "lps");
+ }
+ return record ? { ...j, [junctionText]: val } : j;
+ });
+ }, [junctionData, compareJunctionCalData, junctionText]);
+
+ const mergedComparePipeData = useMemo(() => {
+ const linkMap = new Map(comparePipeCalData.map((r: any) => [r.ID, r]));
+ return pipeData.map((p) => {
+ const record = linkMap.get(p.id);
+ if (!record) return p;
+ const isFlow = pipeText === "flow";
+ let val = record.value;
+ if (val !== undefined && isFlow) {
+ val = toM3h(val, "lps");
+ }
+ return {
+ ...p,
+ [pipeText]: isFlow ? Math.abs(val) : val,
+ flowFlag: isFlow && record.value < 0 ? -1 : 1,
+ path: isFlow && record.value < 0 ? [...p.path].reverse() : p.path,
+ };
+ });
+ }, [pipeData, comparePipeCalData, pipeText]);
+
const [diameterRange, setDiameterRange] = useState<
[number, number] | undefined
>();
@@ -208,6 +262,24 @@ const MapComponent: React.FC = ({ children }) => {
[number, number] | undefined
>();
+ const toggleCompareMode = useCallback(() => {
+ setCompareMode((prev) => !prev);
+ }, []);
+
+ const maps = useMemo(
+ () =>
+ [map, isCompareMode ? compareMap : undefined].filter(Boolean) as OlMap[],
+ [compareMap, isCompareMode, map],
+ );
+
+ const deckLayers = useMemo(
+ () =>
+ [deckLayer, isCompareMode ? compareDeckLayer : undefined].filter(
+ Boolean,
+ ) as DeckLayer[],
+ [compareDeckLayer, deckLayer, isCompareMode],
+ );
+
const setJunctionData = (newData: any[]) => {
const uniqueNewData = newData.filter((item) => {
if (!item || !item.id) return false;
@@ -518,6 +590,178 @@ const MapComponent: React.FC = ({ children }) => {
},
});
+ const createOperationalLayers = () => {
+ const nextJunctionSource = new VectorTileSource({
+ url: `${MAP_URL}/gwc/service/tms/1.0.0/${MAP_WORKSPACE}:geo_junctions@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`,
+ format: new MVT(),
+ projection: "EPSG:3857",
+ });
+ const nextPipeSource = new VectorTileSource({
+ url: `${MAP_URL}/gwc/service/tms/1.0.0/${MAP_WORKSPACE}:geo_pipes@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`,
+ format: new MVT(),
+ projection: "EPSG:3857",
+ });
+ const nextJunctionsLayer = new WebGLVectorTileLayer({
+ source: nextJunctionSource as any,
+ style: defaultFlatStyle,
+ extent: MAP_EXTENT,
+ maxZoom: 24,
+ minZoom: 11,
+ properties: {
+ name: "节点",
+ value: "junctions",
+ type: "point",
+ properties: [
+ { name: "高程", value: "elevation" },
+ { name: "实际需水量", value: "actual_demand" },
+ { name: "水头", value: "total_head" },
+ { name: "压力", value: "pressure" },
+ { name: "水质", value: "quality" },
+ ],
+ },
+ });
+ const nextPipesLayer = new WebGLVectorTileLayer({
+ source: nextPipeSource as any,
+ style: defaultFlatStyle,
+ extent: MAP_EXTENT,
+ maxZoom: 24,
+ minZoom: 11,
+ properties: {
+ name: "管道",
+ value: "pipes",
+ type: "linestring",
+ properties: [
+ { name: "管径", value: "diameter" },
+ { name: "流量", value: "flow" },
+ { name: "摩阻系数", value: "friction" },
+ { name: "水头损失", value: "headloss" },
+ { name: "单位水头损失", value: "unit_headloss" },
+ { name: "水质", value: "quality" },
+ { name: "反应速率", value: "reaction" },
+ { name: "设置值", value: "setting" },
+ { name: "状态", value: "status" },
+ { name: "流速", value: "velocity" },
+ ],
+ },
+ });
+ const nextValvesLayer = new WebGLVectorTileLayer({
+ source: valveSource as any,
+ style: valveStyle,
+ extent: MAP_EXTENT,
+ maxZoom: 24,
+ minZoom: 16,
+ properties: {
+ name: "阀门",
+ value: "valves",
+ type: "linestring",
+ properties: [],
+ },
+ });
+ const nextReservoirsLayer = new VectorLayer({
+ source: reservoirSource,
+ style: reservoirStyle,
+ extent: MAP_EXTENT,
+ maxZoom: 24,
+ minZoom: 11,
+ properties: {
+ name: "水库",
+ value: "reservoirs",
+ type: "point",
+ properties: [],
+ },
+ });
+ const nextPumpsLayer = new VectorLayer({
+ source: pumpSource,
+ style: pumpStyle,
+ extent: MAP_EXTENT,
+ maxZoom: 24,
+ minZoom: 11,
+ properties: {
+ name: "水泵",
+ value: "pumps",
+ type: "linestring",
+ properties: [],
+ },
+ });
+ const nextTanksLayer = new VectorLayer({
+ source: tankSource,
+ style: tankStyle,
+ extent: MAP_EXTENT,
+ maxZoom: 24,
+ minZoom: 11,
+ properties: {
+ name: "水箱",
+ value: "tanks",
+ type: "point",
+ properties: [],
+ },
+ });
+ const nextScadaLayer = new VectorLayer({
+ source: scadaSource,
+ style: scadaStyle,
+ extent: MAP_EXTENT,
+ maxZoom: 24,
+ minZoom: 11,
+ properties: {
+ name: "SCADA",
+ value: "scada",
+ type: "point",
+ properties: [],
+ },
+ });
+
+ const availableLayers: any[] = [];
+ config.MAP_AVAILABLE_LAYERS.forEach((layerValue) => {
+ switch (layerValue) {
+ case "junctions":
+ availableLayers.push(nextJunctionsLayer);
+ break;
+ case "pipes":
+ availableLayers.push(nextPipesLayer);
+ break;
+ case "valves":
+ availableLayers.push(nextValvesLayer);
+ break;
+ case "reservoirs":
+ availableLayers.push(nextReservoirsLayer);
+ break;
+ case "pumps":
+ availableLayers.push(nextPumpsLayer);
+ break;
+ case "tanks":
+ availableLayers.push(nextTanksLayer);
+ break;
+ case "scada":
+ availableLayers.push(nextScadaLayer);
+ break;
+ }
+ });
+ availableLayers.sort((a, b) => {
+ const order = [
+ "valves",
+ "junctions",
+ "scada",
+ "reservoirs",
+ "pumps",
+ "tanks",
+ "pipes",
+ ].reverse();
+ const getValue = (layer: any) => {
+ const props = layer.get ? layer.get("properties") : undefined;
+ return (props && props.value) || layer.get?.("value") || "";
+ };
+ const aVal = getValue(a);
+ const bVal = getValue(b);
+ let ia = order.indexOf(aVal);
+ let ib = order.indexOf(bVal);
+ if (ia === -1) ia = order.length;
+ if (ib === -1) ib = order.length;
+ return ia - ib;
+ });
+
+ return availableLayers;
+ };
+
// The map and layer instances are intentionally rebuilt only when workspace or extent changes.
useEffect(() => {
if (!mapRef.current) return;
@@ -857,148 +1101,284 @@ const MapComponent: React.FC = ({ children }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [MAP_WORKSPACE, MAP_EXTENT]);
+ useEffect(() => {
+ if (!isCompareMode) {
+ isCompareDisposingRef.current = true;
+ setCompareJunctionCalData([]);
+ setComparePipeCalData([]);
+ return;
+ }
+ if (!map || !compareMapRef.current || !compareCanvasRef.current) return;
+
+ isCompareDisposingRef.current = false;
+ const availableLayers = createOperationalLayers();
+ const nextCompareMap = new OlMap({
+ target: compareMapRef.current,
+ view: map.getView(),
+ layers: availableLayers.slice(),
+ controls: [],
+ });
+ nextCompareMap.getAllLayers().forEach((layer) => {
+ const layerId = layer.get("value");
+ if (!layerId) return;
+ const primaryLayer = map
+ .getAllLayers()
+ .find((currentLayer) => currentLayer.get("value") === layerId);
+ if (primaryLayer) {
+ layer.setVisible(primaryLayer.getVisible());
+ }
+ });
+ setCompareMap(nextCompareMap);
+
+ const compareDeck = new Deck({
+ initialViewState: {
+ longitude: 0,
+ latitude: 0,
+ zoom: 1,
+ },
+ canvas: compareCanvasRef.current,
+ controller: false,
+ layers: [],
+ });
+ const nextCompareDeckLayer = new DeckLayer(
+ compareDeck,
+ compareCanvasRef.current,
+ {
+ name: "compareDeckLayer",
+ value: "deckLayer",
+ },
+ );
+ compareDeckLayerRef.current = nextCompareDeckLayer;
+ setCompareDeckLayer(nextCompareDeckLayer);
+ nextCompareMap.addLayer(nextCompareDeckLayer);
+
+ const resizeTimerId = window.setTimeout(() => {
+ map.updateSize();
+ nextCompareMap.updateSize();
+ }, 0);
+
+ return () => {
+ isCompareDisposingRef.current = true;
+ window.clearTimeout(resizeTimerId);
+ if (
+ compareDeckLayerRef.current &&
+ !compareDeckLayerRef.current.isDisposedLayer()
+ ) {
+ try {
+ nextCompareMap.removeLayer(compareDeckLayerRef.current);
+ } catch {
+ // Layer may have already been removed during teardown.
+ }
+ compareDeckLayerRef.current.disposeDeck();
+ }
+ compareDeckLayerRef.current = null;
+ setCompareDeckLayer(undefined);
+ setCompareMap(undefined);
+ nextCompareMap.setTarget(undefined);
+ nextCompareMap.dispose();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isCompareMode, map]);
+
+ useEffect(() => {
+ const resizeTimerId = window.setTimeout(() => {
+ map?.updateSize();
+ compareMap?.updateSize();
+ }, 0);
+
+ return () => {
+ window.clearTimeout(resizeTimerId);
+ };
+ }, [compareMap, isCompareMode, map]);
+
// 当数据变化时,更新 deck.gl 图层
useEffect(() => {
- if (isDisposingRef.current) return;
- const deckLayer = deckLayerRef.current;
- if (!deckLayer) return; // 如果 deck 实例还未创建,则退出
- if (deckLayer.isDisposedLayer()) return;
- if (!mergedJunctionData.length) return;
- if (!mergedPipeData.length) return;
- const junctionTextLayer = new TextLayer({
- id: "junctionTextLayer",
- name: "节点文字",
- zIndex: 10,
- data: mergedJunctionData,
- getPosition: (d: any) => d.position,
- fontFamily: "Monaco, monospace",
- getText: (d: any) => {
- let idPart = showJunctionId ? d.id : "";
- let propPart = "";
- if (showJunctionTextLayer && d[junctionText] !== undefined) {
- const value = (d[junctionText] as number).toFixed(3);
- propPart = `${value}`;
- }
- if (idPart && propPart) return `${idPart} - ${propPart}`;
- return idPart || propPart;
- },
- getSize: 14,
- fontWeight: "bold",
- getColor: [33, 37, 41], // 深灰色,在灰白背景上清晰可见
- getAngle: 0,
- getTextAnchor: "middle",
- getAlignmentBaseline: "center",
- getPixelOffset: [0, -10],
- visible:
+ const syncDeckOverlay = (
+ targetDeckLayer: DeckLayer | null,
+ targetJunctionData: any[],
+ targetPipeData: any[],
+ disposing: boolean,
+ ) => {
+ if (disposing || !targetDeckLayer || targetDeckLayer.isDisposedLayer()) {
+ return;
+ }
+ const shouldShowJunctionText =
(showJunctionTextLayer || showJunctionId) &&
currentZoom >= 15 &&
- currentZoom <= 24,
- updateTriggers: {
- getText: [showJunctionId, showJunctionTextLayer, junctionText],
- },
- extensions: [new CollisionFilterExtension()],
- collisionTestProps: {
- sizeScale: 3,
- },
- characterSet: "auto",
- fontSettings: {
- sdf: true,
- fontSize: 64,
- buffer: 6,
- },
- // outlineWidth: 3,
- // outlineColor: [255, 255, 255, 220],
- });
-
- const pipeTextLayer = new TextLayer({
- id: "pipeTextLayer",
- name: "管道文字",
- zIndex: 10,
- data: mergedPipeData,
- getPosition: (d: any) => d.position,
- fontFamily: "Monaco, monospace",
- getText: (d: any) => {
- let idPart = showPipeId ? d.id : "";
- let propPart = "";
- if (showPipeTextLayer && d[pipeText] !== undefined) {
- let value;
- if (pipeText === "unit_headloss") {
- value = (
- (d["unit_headloss"] / (d["length"] / 1000)) as number
- ).toFixed(3);
- } else {
- value = Math.abs(d[pipeText] as number).toFixed(3);
- }
- propPart = `${value}`;
- }
- if (idPart && propPart) return `${idPart} - ${propPart}`;
- return idPart || propPart;
- },
- getSize: 14,
- fontWeight: "bold",
- getColor: [33, 37, 41], // 深灰色
- getAngle: (d: any) => d.angle || 0,
- getPixelOffset: [0, -8],
- getTextAnchor: "middle",
- getAlignmentBaseline: "bottom",
- visible:
+ currentZoom <= 24 &&
+ targetJunctionData.length > 0;
+ const shouldShowPipeText =
(showPipeTextLayer || showPipeId) &&
currentZoom >= 15 &&
- currentZoom <= 24,
- updateTriggers: {
- getText: [showPipeId, showPipeTextLayer, pipeText],
- },
- extensions: [new CollisionFilterExtension()],
- collisionTestProps: {
- sizeScale: 3,
- },
- characterSet: "auto",
- fontSettings: {
- sdf: true,
- fontSize: 64,
- buffer: 6,
- },
- // outlineWidth: 3,
- // outlineColor: [255, 255, 255, 220],
- });
+ currentZoom <= 24 &&
+ targetPipeData.length > 0;
+ const shouldShowContour =
+ showContourLayer &&
+ currentZoom >= 11 &&
+ currentZoom <= 24 &&
+ targetJunctionData.length > 0;
- const contourLayer = new ContourLayer({
- id: "junctionContourLayer",
- name: "等值线",
- data: mergedJunctionData,
- aggregation: "MEAN",
- cellSize: 600,
- strokeWidth: 0,
- contours: contours,
- getPosition: (d) => d.position,
- getWeight: (d: any) =>
- (d[junctionText] as number) < 0 ? 0 : (d[junctionText] as number),
- opacity: 1,
- visible: showContourLayer && currentZoom >= 11 && currentZoom <= 24,
- updateTriggers: {
- // 当 mergedJunctionData 内部数据更新时,通知 getWeight 重新计算
- getWeight: [mergedJunctionData, junctionText],
- },
- });
- if (deckLayer.getDeckLayerById("junctionTextLayer")) {
- // 传入完整 layer 实例以保证 clone/替换时保留 layer 类型和方法
- deckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer);
- } else {
- deckLayer.addDeckLayer(junctionTextLayer);
- }
- if (deckLayer.getDeckLayerById("pipeTextLayer")) {
- deckLayer.updateDeckLayer("pipeTextLayer", pipeTextLayer);
- } else {
- deckLayer.addDeckLayer(pipeTextLayer);
- }
- if (deckLayer.getDeckLayerById("junctionContourLayer")) {
- deckLayer.updateDeckLayer("junctionContourLayer", contourLayer);
- } else {
- deckLayer.addDeckLayer(contourLayer);
+ if (!shouldShowJunctionText) {
+ targetDeckLayer.removeDeckLayer("junctionTextLayer");
+ }
+ if (!shouldShowPipeText) {
+ targetDeckLayer.removeDeckLayer("pipeTextLayer");
+ }
+ if (!shouldShowContour) {
+ targetDeckLayer.removeDeckLayer("junctionContourLayer");
+ }
+ if (!shouldShowJunctionText && !shouldShowPipeText && !shouldShowContour) {
+ return;
+ }
+
+ const junctionTextLayer = shouldShowJunctionText
+ ? new TextLayer({
+ id: "junctionTextLayer",
+ name: "节点文字",
+ zIndex: 10,
+ data: targetJunctionData,
+ getPosition: (d: any) => d.position,
+ fontFamily: "Monaco, monospace",
+ getText: (d: any) => {
+ let idPart = showJunctionId ? d.id : "";
+ let propPart = "";
+ if (showJunctionTextLayer && d[junctionText] !== undefined) {
+ const value = (d[junctionText] as number).toFixed(3);
+ propPart = `${value}`;
+ }
+ if (idPart && propPart) return `${idPart} - ${propPart}`;
+ return idPart || propPart;
+ },
+ getSize: 14,
+ fontWeight: "bold",
+ getColor: [33, 37, 41],
+ getAngle: 0,
+ getTextAnchor: "middle",
+ getAlignmentBaseline: "center",
+ getPixelOffset: [0, -10],
+ visible: true,
+ updateTriggers: {
+ getText: [showJunctionId, showJunctionTextLayer, junctionText],
+ },
+ extensions: [new CollisionFilterExtension()],
+ collisionTestProps: {
+ sizeScale: 3,
+ },
+ characterSet: "auto",
+ fontSettings: {
+ sdf: true,
+ fontSize: 64,
+ buffer: 6,
+ },
+ })
+ : null;
+
+ const pipeTextLayer = shouldShowPipeText
+ ? new TextLayer({
+ id: "pipeTextLayer",
+ name: "管道文字",
+ zIndex: 10,
+ data: targetPipeData,
+ getPosition: (d: any) => d.position,
+ fontFamily: "Monaco, monospace",
+ getText: (d: any) => {
+ let idPart = showPipeId ? d.id : "";
+ let propPart = "";
+ if (showPipeTextLayer && d[pipeText] !== undefined) {
+ let value;
+ if (pipeText === "unit_headloss") {
+ value = (
+ (d["unit_headloss"] / (d["length"] / 1000)) as number
+ ).toFixed(3);
+ } else {
+ value = Math.abs(d[pipeText] as number).toFixed(3);
+ }
+ propPart = `${value}`;
+ }
+ if (idPart && propPart) return `${idPart} - ${propPart}`;
+ return idPart || propPart;
+ },
+ getSize: 14,
+ fontWeight: "bold",
+ getColor: [33, 37, 41],
+ getAngle: (d: any) => d.angle || 0,
+ getPixelOffset: [0, -8],
+ getTextAnchor: "middle",
+ getAlignmentBaseline: "bottom",
+ visible: true,
+ updateTriggers: {
+ getText: [showPipeId, showPipeTextLayer, pipeText],
+ },
+ extensions: [new CollisionFilterExtension()],
+ collisionTestProps: {
+ sizeScale: 3,
+ },
+ characterSet: "auto",
+ fontSettings: {
+ sdf: true,
+ fontSize: 64,
+ buffer: 6,
+ },
+ })
+ : null;
+
+ const contourLayer = shouldShowContour
+ ? new ContourLayer({
+ id: "junctionContourLayer",
+ name: "等值线",
+ data: targetJunctionData,
+ aggregation: "MEAN",
+ cellSize: 600,
+ strokeWidth: 0,
+ contours: contours,
+ getPosition: (d) => d.position,
+ getWeight: (d: any) =>
+ (d[junctionText] as number) < 0 ? 0 : (d[junctionText] as number),
+ opacity: 1,
+ visible: true,
+ updateTriggers: {
+ getWeight: [targetJunctionData, junctionText],
+ },
+ })
+ : null;
+
+ if (junctionTextLayer && targetDeckLayer.getDeckLayerById("junctionTextLayer")) {
+ targetDeckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer);
+ } else if (junctionTextLayer) {
+ targetDeckLayer.addDeckLayer(junctionTextLayer);
+ }
+ if (pipeTextLayer && targetDeckLayer.getDeckLayerById("pipeTextLayer")) {
+ targetDeckLayer.updateDeckLayer("pipeTextLayer", pipeTextLayer);
+ } else if (pipeTextLayer) {
+ targetDeckLayer.addDeckLayer(pipeTextLayer);
+ }
+ if (contourLayer && targetDeckLayer.getDeckLayerById("junctionContourLayer")) {
+ targetDeckLayer.updateDeckLayer("junctionContourLayer", contourLayer);
+ } else if (contourLayer) {
+ targetDeckLayer.addDeckLayer(contourLayer);
+ }
+ };
+
+ syncDeckOverlay(
+ deckLayerRef.current,
+ mergedJunctionData,
+ mergedPipeData,
+ isDisposingRef.current,
+ );
+ if (isCompareMode) {
+ syncDeckOverlay(
+ compareDeckLayerRef.current,
+ mergedCompareJunctionData,
+ mergedComparePipeData,
+ isCompareDisposingRef.current,
+ );
}
}, [
mergedJunctionData,
mergedPipeData,
+ mergedCompareJunctionData,
+ mergedComparePipeData,
+ isCompareMode,
junctionText,
pipeText,
currentZoom,
@@ -1012,57 +1392,69 @@ const MapComponent: React.FC = ({ children }) => {
// 控制流动动画开关
useEffect(() => {
- if (isDisposingRef.current) return;
- if (pipeText === "flow" && currentPipeCalData.length > 0) {
- flowAnimation.current = true;
- } else {
- flowAnimation.current = false;
- }
- const deckLayer = deckLayerRef.current;
- if (!deckLayer) return; // 如果 deck 实例还未创建,则退出
+ flowAnimation.current = pipeText === "flow" && currentPipeCalData.length > 0;
+ const shouldShowWaterflow =
+ isWaterflowLayerAvailable &&
+ showWaterflowLayer &&
+ flowAnimation.current &&
+ currentZoom >= 12 &&
+ currentZoom <= 24;
- let animationFrameId: number; // 保存 requestAnimationFrame 的 ID
+ let animationFrameId: number;
- // 动画循环
- const animate = () => {
- if (isDisposingRef.current || deckLayer.isDisposedLayer()) return;
- // 动画总时长(秒)
+ const syncWaterflowLayer = (
+ targetDeckLayer: DeckLayer | null,
+ targetPipeData: any[],
+ disposing: boolean,
+ ) => {
+ if (disposing || !targetDeckLayer || targetDeckLayer.isDisposedLayer()) {
+ return;
+ }
+ if (!shouldShowWaterflow || targetPipeData.length === 0) {
+ targetDeckLayer.removeDeckLayer("waterflowLayer");
+ return;
+ }
const animationDuration = 10;
const bufferTime = 2;
const loopLength = animationDuration + bufferTime;
- const currentTime = (Date.now() / 1000) % loopLength;
+ const currentFrameTime = (Date.now() / 1000) % loopLength;
const waterflowLayer = new TripsLayer({
id: "waterflowLayer",
name: "水流",
- data: mergedPipeData,
+ data: targetPipeData,
getPath: (d) => d.path,
- getTimestamps: (d) => {
- return d.timestamps; // 这些应该是与 currentTime 匹配的数值
- },
+ getTimestamps: (d) => d.timestamps,
getColor: [0, 220, 255],
opacity: 0.8,
- visible:
- isWaterflowLayerAvailable &&
- showWaterflowLayer &&
- flowAnimation.current && // 保持动画标志作为可见性的一部分
- currentZoom >= 12 &&
- currentZoom <= 24,
+ visible: true,
widthMinPixels: 5,
- jointRounded: true, // 拐角变圆
- // capRounded: true, // 端点变圆
- trailLength: 2, // 水流尾迹淡出时间
- currentTime: currentTime,
+ jointRounded: true,
+ trailLength: 2,
+ currentTime: currentFrameTime,
});
- if (deckLayer.getDeckLayerById("waterflowLayer")) {
- deckLayer.updateDeckLayer("waterflowLayer", waterflowLayer);
+ if (targetDeckLayer.getDeckLayerById("waterflowLayer")) {
+ targetDeckLayer.updateDeckLayer("waterflowLayer", waterflowLayer);
} else {
- deckLayer.addDeckLayer(waterflowLayer);
+ targetDeckLayer.addDeckLayer(waterflowLayer);
}
+ };
- // 只有在需要动画时才请求下一帧,但图层已经添加到了 deckLayer 中
- if (flowAnimation.current) {
+ const animate = () => {
+ syncWaterflowLayer(
+ deckLayerRef.current,
+ mergedPipeData,
+ isDisposingRef.current,
+ );
+ if (isCompareMode) {
+ syncWaterflowLayer(
+ compareDeckLayerRef.current,
+ mergedComparePipeData,
+ isCompareDisposingRef.current,
+ );
+ }
+ if (shouldShowWaterflow) {
animationFrameId = requestAnimationFrame(animate);
}
};
@@ -1078,6 +1470,8 @@ const MapComponent: React.FC = ({ children }) => {
currentPipeCalData,
currentZoom,
mergedPipeData,
+ mergedComparePipeData,
+ isCompareMode,
pipeText,
isWaterflowLayerAvailable,
showWaterflowLayer,
@@ -1097,6 +1491,13 @@ const MapComponent: React.FC = ({ children }) => {
setCurrentJunctionCalData,
currentPipeCalData,
setCurrentPipeCalData,
+ compareJunctionCalData,
+ setCompareJunctionCalData,
+ comparePipeCalData,
+ setComparePipeCalData,
+ isCompareMode,
+ setCompareMode,
+ toggleCompareMode,
setShowJunctionTextLayer,
setShowPipeTextLayer,
setShowJunctionId,
@@ -1115,17 +1516,50 @@ const MapComponent: React.FC = ({ children }) => {
pipeText,
setContours,
deckLayer,
+ compareDeckLayer,
+ deckLayers,
+ compareMap,
+ maps,
diameterRange,
elevationRange,
}}
>
-
+
+
+
+
+ {isCompareMode && (
+
+ 方案模拟
+
+ )}
+
+ {isCompareMode && (
+
+ )}
+
+ {isCompareMode && (
+
+ )}
{children}
-
>