添加对比模式功能,优化地图组件
This commit is contained in:
@@ -8,7 +8,11 @@ export default function Home() {
|
||||
return (
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
<MapComponent>
|
||||
<MapToolbar queryType="scheme" schemeType="burst_analysis" />
|
||||
<MapToolbar
|
||||
queryType="scheme"
|
||||
schemeType="burst_analysis"
|
||||
enableCompare
|
||||
/>
|
||||
<BurstPipeAnalysisPanel />
|
||||
</MapComponent>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,11 @@ export default function Home() {
|
||||
return (
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
<MapComponent>
|
||||
<MapToolbar queryType="scheme" schemeType="contaminant_analysis" />
|
||||
<MapToolbar
|
||||
queryType="scheme"
|
||||
schemeType="contaminant_analysis"
|
||||
enableCompare
|
||||
/>
|
||||
<WaterQualityPanel />
|
||||
</MapComponent>
|
||||
</div>
|
||||
|
||||
@@ -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:
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
|
||||
}),
|
||||
});
|
||||
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:
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
|
||||
}),
|
||||
});
|
||||
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:
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
|
||||
}),
|
||||
});
|
||||
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:
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>',
|
||||
}),
|
||||
});
|
||||
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: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
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: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
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: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
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: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
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}`,
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>'
|
||||
);
|
||||
const lightMapLayer = createTileLayer(
|
||||
`https://api.mapbox.com/styles/v1/mapbox/light-v11/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>'
|
||||
);
|
||||
const satelliteLayer = createTileLayer(
|
||||
`https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>'
|
||||
);
|
||||
const satelliteStreetsLayer = createTileLayer(
|
||||
`https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v12/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
|
||||
'数据来源:<a href="https://www.mapbox.com/">Mapbox</a> & <a href="https://www.openstreetmap.org/">OpenStreetMap</a>'
|
||||
);
|
||||
|
||||
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: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
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: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
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: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
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: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||
}),
|
||||
});
|
||||
|
||||
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<OlMap, ReturnType<typeof createBaseLayerEntries>>());
|
||||
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<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleEnter = () => {
|
||||
@@ -217,7 +232,7 @@ const BaseLayers: React.FC = () => {
|
||||
{isExpanded && (
|
||||
<div
|
||||
className={clsx(
|
||||
"absolute flex right-24 bottom-0 w-90 h-25 bg-white rounded-xl drop-shadow-xl shadow-black transition-all duration-300",
|
||||
"absolute flex right-24 bottom-0 w-132 h-25 bg-white rounded-xl drop-shadow-xl shadow-black transition-all duration-300",
|
||||
isShow ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
onMouseEnter={handleEnter}
|
||||
@@ -226,7 +241,7 @@ const BaseLayers: React.FC = () => {
|
||||
{baseLayers.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
className="flex flex-auto flex-col justify-center items-center text-gray-500 text-xs"
|
||||
className="flex flex-auto flex-col justify-center items-center text-gray-500 text-xs"
|
||||
onClick={() => handleMapLayers(item.id)}
|
||||
>
|
||||
<Image
|
||||
|
||||
@@ -5,6 +5,7 @@ import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
||||
import VectorLayer from "ol/layer/Vector";
|
||||
import VectorTileLayer from "ol/layer/VectorTile";
|
||||
import { DeckLayer } from "@utils/layers";
|
||||
import type { Map as OlMap } from "ol";
|
||||
|
||||
// 定义统一的图层项接口
|
||||
interface LayerItem {
|
||||
@@ -30,8 +31,10 @@ const LAYER_ORDER = [
|
||||
const LayerControl: React.FC = () => {
|
||||
const map = useMap();
|
||||
const data = useData();
|
||||
const maps: OlMap[] = data?.maps?.length ? data.maps : map ? [map] : [];
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const deckLayer = data?.deckLayer;
|
||||
const deckLayers = data?.deckLayers ?? (deckLayer ? [deckLayer] : []);
|
||||
const isContourLayerAvailable = data?.isContourLayerAvailable;
|
||||
const isWaterflowLayerAvailable = data?.isWaterflowLayerAvailable;
|
||||
const setShowWaterflowLayer = data?.setShowWaterflowLayer;
|
||||
@@ -117,8 +120,16 @@ const LayerControl: React.FC = () => {
|
||||
|
||||
const handleVisibilityChange = (item: LayerItem, checked: boolean) => {
|
||||
if (item.type === "ol") {
|
||||
item.layerRef.setVisible(checked);
|
||||
} else if (item.type === "deck" && deckLayer) {
|
||||
maps.forEach((targetMap) => {
|
||||
targetMap
|
||||
.getAllLayers()
|
||||
.filter((layer) => layer.get("value") === item.id)
|
||||
.forEach((layer) => layer.setVisible(checked));
|
||||
});
|
||||
} else if (item.type === "deck" && deckLayers.length > 0) {
|
||||
deckLayers.forEach((targetDeckLayer) => {
|
||||
targetDeckLayer.setDeckLayerVisible(item.id, checked);
|
||||
});
|
||||
if (item.id === "junctionContourLayer") {
|
||||
setShowContourLayer && setShowContourLayer(checked);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { FlatStyleLike } from "ol/style/flat";
|
||||
import { calculateClassification } from "@utils/breaks_classification";
|
||||
import { parseColor } from "@utils/parseColor";
|
||||
import { VectorTile } from "ol";
|
||||
import type { Map as OlMap } from "ol";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
import { config } from "@/config/config";
|
||||
|
||||
@@ -182,6 +183,13 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
const data = useData();
|
||||
const currentJunctionCalData = data?.currentJunctionCalData;
|
||||
const currentPipeCalData = data?.currentPipeCalData;
|
||||
const compareJunctionCalData = data?.compareJunctionCalData;
|
||||
const comparePipeCalData = data?.comparePipeCalData;
|
||||
const compareMap = data?.compareMap;
|
||||
const activeMaps = useMemo<OlMap[]>(
|
||||
() => (data?.maps?.length ? data.maps : map ? [map] : []),
|
||||
[data?.maps, map]
|
||||
);
|
||||
const junctionText = data?.junctionText ?? "";
|
||||
const pipeText = data?.pipeText ?? "";
|
||||
const setShowJunctionTextLayer = data?.setShowJunctionTextLayer;
|
||||
@@ -229,6 +237,45 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
customColors: [],
|
||||
});
|
||||
|
||||
const getRenderLayersById = useCallback(
|
||||
(layerId: string) =>
|
||||
activeMaps.flatMap((targetMap) =>
|
||||
targetMap
|
||||
.getAllLayers()
|
||||
.filter((layer) => layer.get("value") === layerId)
|
||||
.filter((layer): layer is WebGLVectorTileLayer => layer instanceof WebGLVectorTileLayer)
|
||||
),
|
||||
[activeMaps]
|
||||
);
|
||||
|
||||
const getMapKey = useCallback((targetMap: OlMap, layerId: string) => {
|
||||
const mapUid = (targetMap as unknown as { ol_uid?: string }).ol_uid || "map";
|
||||
return `${mapUid}:${layerId}`;
|
||||
}, []);
|
||||
|
||||
const getDataForMap = useCallback(
|
||||
(targetMap: OlMap, layerId: string) => {
|
||||
if (layerId === "junctions") {
|
||||
return targetMap === compareMap
|
||||
? compareJunctionCalData || []
|
||||
: currentJunctionCalData || [];
|
||||
}
|
||||
if (layerId === "pipes") {
|
||||
return targetMap === compareMap
|
||||
? comparePipeCalData || []
|
||||
: currentPipeCalData || [];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
[
|
||||
compareJunctionCalData,
|
||||
compareMap,
|
||||
comparePipeCalData,
|
||||
currentJunctionCalData,
|
||||
currentPipeCalData,
|
||||
]
|
||||
);
|
||||
|
||||
const getDefaultCustomColors = (
|
||||
segments: number,
|
||||
existingColors: string[] = []
|
||||
@@ -613,13 +660,10 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
return;
|
||||
}
|
||||
const styleConfig = layerStyleConfig.styleConfig;
|
||||
const renderLayer = renderLayers.filter((layer) => {
|
||||
return layer.get("value") === layerStyleConfig.layerId;
|
||||
})[0];
|
||||
const targetLayers = getRenderLayersById(layerStyleConfig.layerId);
|
||||
const renderLayer = targetLayers[0];
|
||||
if (!renderLayer || !styleConfig?.property) return;
|
||||
const layerType: string = renderLayer?.get("type");
|
||||
const source = renderLayer.getSource();
|
||||
if (!source) return;
|
||||
const layerType: string = renderLayer.get("type");
|
||||
|
||||
const breaksLength = breaks.length;
|
||||
// 根据 breaks 计算每个分段的颜色,线条粗细
|
||||
@@ -757,7 +801,9 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
dynamicStyle["circle-stroke-width"] = 2;
|
||||
}
|
||||
// 应用样式到图层
|
||||
renderLayer.setStyle(dynamicStyle);
|
||||
targetLayers.forEach((targetLayer) => {
|
||||
targetLayer.setStyle(dynamicStyle);
|
||||
});
|
||||
// 用初始化时的样式配置更新图例配置,避免覆盖已有的图例名称和属性
|
||||
const layerId = renderLayer.get("value");
|
||||
const initLayerStyleState = layerStyleStates.find(
|
||||
@@ -844,10 +890,12 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
if (!selectedRenderLayer) return;
|
||||
// 重置 WebGL 图层样式
|
||||
const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE;
|
||||
selectedRenderLayer.setStyle(defaultFlatStyle);
|
||||
const layerId = selectedRenderLayer.get("value");
|
||||
getRenderLayersById(layerId).forEach((targetLayer) => {
|
||||
targetLayer.setStyle(defaultFlatStyle);
|
||||
});
|
||||
|
||||
// 删除对应图层的样式状态,从而移除图例显示
|
||||
const layerId = selectedRenderLayer.get("value");
|
||||
if (layerId !== undefined) {
|
||||
setLayerStyleStates((prev) =>
|
||||
prev.filter((state) => state.layerId !== layerId)
|
||||
@@ -870,11 +918,15 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
}
|
||||
};
|
||||
// 更新当前 VectorTileSource 中的所有缓冲要素属性
|
||||
const updateVectorTileSource = (property: string, data: any[]) => {
|
||||
if (!map) return;
|
||||
const vectorTileSources = map
|
||||
const updateVectorTileSource = (
|
||||
targetMap: OlMap,
|
||||
layerId: string,
|
||||
property: string,
|
||||
data: any[]
|
||||
) => {
|
||||
const vectorTileSources = targetMap
|
||||
.getAllLayers()
|
||||
.filter((layer) => layer instanceof WebGLVectorTileLayer)
|
||||
.filter((layer) => layer.get("value") === layerId)
|
||||
.map((layer) => layer.getSource() as VectorTileSource)
|
||||
.filter((source) => source);
|
||||
|
||||
@@ -911,16 +963,16 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
};
|
||||
// 新增事件,监听 VectorTileSource 的 tileloadend 事件,为新增瓦片数据动态更新要素属性
|
||||
const tileLoadListenersRef = useRef<
|
||||
Map<VectorTileSource, (event: any) => void>
|
||||
Map<string, { source: VectorTileSource; listener: (event: any) => void }>
|
||||
>(new Map());
|
||||
|
||||
const attachVectorTileSourceLoadedEvent = (
|
||||
targetMap: OlMap,
|
||||
layerId: string,
|
||||
property: string,
|
||||
data: any[]
|
||||
) => {
|
||||
if (!map) return;
|
||||
const vectorTileSource = map
|
||||
const vectorTileSource = targetMap
|
||||
.getAllLayers()
|
||||
.filter((layer) => layer.get("value") === layerId)
|
||||
.map((layer) => layer.getSource() as VectorTileSource)
|
||||
@@ -956,24 +1008,25 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const listenerKey = getMapKey(targetMap, layerId);
|
||||
vectorTileSource.on("tileloadend", listener);
|
||||
tileLoadListenersRef.current.set(vectorTileSource, listener);
|
||||
tileLoadListenersRef.current.set(listenerKey, {
|
||||
source: vectorTileSource,
|
||||
listener,
|
||||
});
|
||||
};
|
||||
// 新增函数:取消对应 layerId 已添加的 on 事件
|
||||
const removeVectorTileSourceLoadedEvent = (layerId: string) => {
|
||||
if (!map) return;
|
||||
const vectorTileSource = map
|
||||
.getAllLayers()
|
||||
.filter((layer) => layer.get("value") === layerId)
|
||||
.map((layer) => layer.getSource() as VectorTileSource)
|
||||
.filter((source) => source)[0];
|
||||
if (!vectorTileSource) return;
|
||||
const listener = tileLoadListenersRef.current.get(vectorTileSource);
|
||||
if (listener) {
|
||||
vectorTileSource.un("tileloadend", listener);
|
||||
tileLoadListenersRef.current.delete(vectorTileSource);
|
||||
}
|
||||
};
|
||||
const removeVectorTileSourceLoadedEvent = useCallback(
|
||||
(targetMap: OlMap, layerId: string) => {
|
||||
const listenerKey = getMapKey(targetMap, layerId);
|
||||
const listenerState = tileLoadListenersRef.current.get(listenerKey);
|
||||
if (listenerState) {
|
||||
listenerState.source.un("tileloadend", listenerState.listener);
|
||||
tileLoadListenersRef.current.delete(listenerKey);
|
||||
}
|
||||
},
|
||||
[getMapKey]
|
||||
);
|
||||
|
||||
// 监听数据变化,重新应用样式。由样式应用按钮触发,或由数据变化触发
|
||||
useEffect(() => {
|
||||
@@ -998,20 +1051,24 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
);
|
||||
|
||||
if (isElevation) {
|
||||
removeVectorTileSourceLoadedEvent("junctions");
|
||||
activeMaps.forEach((targetMap) => {
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentJunctionCalData) return;
|
||||
// 更新现有的 VectorTileSource
|
||||
updateVectorTileSource(junctionText, currentJunctionCalData);
|
||||
// 移除旧的监听器,并添加新的监听器
|
||||
removeVectorTileSourceLoadedEvent("junctions");
|
||||
attachVectorTileSourceLoadedEvent(
|
||||
"junctions",
|
||||
junctionText,
|
||||
currentJunctionCalData
|
||||
);
|
||||
activeMaps.forEach((targetMap) => {
|
||||
const targetData = getDataForMap(targetMap, "junctions");
|
||||
if (!targetData || targetData.length === 0) return;
|
||||
updateVectorTileSource(targetMap, "junctions", junctionText, targetData);
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||
attachVectorTileSourceLoadedEvent(
|
||||
targetMap,
|
||||
"junctions",
|
||||
junctionText,
|
||||
targetData
|
||||
);
|
||||
});
|
||||
};
|
||||
const updatePipeStyle = () => {
|
||||
const pipeStyleConfigState = layerStyleStates.find(
|
||||
@@ -1023,16 +1080,24 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
applyClassificationStyle("pipes", pipeStyleConfigState?.styleConfig);
|
||||
|
||||
if (isDiameter) {
|
||||
removeVectorTileSourceLoadedEvent("pipes");
|
||||
activeMaps.forEach((targetMap) => {
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentPipeCalData) return;
|
||||
// 更新现有的 VectorTileSource
|
||||
updateVectorTileSource(pipeText, currentPipeCalData);
|
||||
// 移除旧的监听器,并添加新的监听器
|
||||
removeVectorTileSourceLoadedEvent("pipes");
|
||||
attachVectorTileSourceLoadedEvent("pipes", pipeText, currentPipeCalData);
|
||||
activeMaps.forEach((targetMap) => {
|
||||
const targetData = getDataForMap(targetMap, "pipes");
|
||||
if (!targetData || targetData.length === 0) return;
|
||||
updateVectorTileSource(targetMap, "pipes", pipeText, targetData);
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||
attachVectorTileSourceLoadedEvent(
|
||||
targetMap,
|
||||
"pipes",
|
||||
pipeText,
|
||||
targetData
|
||||
);
|
||||
});
|
||||
};
|
||||
if (isUserTrigger) {
|
||||
if (selectedRenderLayer?.get("value") === "junctions") {
|
||||
@@ -1060,10 +1125,14 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
updatePipeStyle();
|
||||
}
|
||||
if (!applyJunctionStyle) {
|
||||
removeVectorTileSourceLoadedEvent("junctions");
|
||||
activeMaps.forEach((targetMap) => {
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||
});
|
||||
}
|
||||
if (!applyPipeStyle) {
|
||||
removeVectorTileSourceLoadedEvent("pipes");
|
||||
activeMaps.forEach((targetMap) => {
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||
});
|
||||
}
|
||||
// This effect is intentionally driven by explicit style triggers and data snapshots.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -1073,8 +1142,20 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||
applyPipeStyle,
|
||||
currentJunctionCalData,
|
||||
currentPipeCalData,
|
||||
compareJunctionCalData,
|
||||
comparePipeCalData,
|
||||
activeMaps,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
activeMaps.forEach((targetMap) => {
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||
});
|
||||
};
|
||||
}, [activeMaps, removeVectorTileSourceLoadedEvent]);
|
||||
|
||||
// 获取地图中的矢量图层,用于选择图层选项
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
@@ -63,6 +63,9 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
const setSelectedDate = data?.setSelectedDate ?? NOOP_SET_SELECTED_DATE;
|
||||
const setCurrentJunctionCalData = data?.setCurrentJunctionCalData;
|
||||
const setCurrentPipeCalData = data?.setCurrentPipeCalData;
|
||||
const setCompareJunctionCalData = data?.setCompareJunctionCalData;
|
||||
const setComparePipeCalData = data?.setComparePipeCalData;
|
||||
const isCompareMode = data?.isCompareMode ?? false;
|
||||
const junctionText = data?.junctionText ?? "";
|
||||
const pipeText = data?.pipeText ?? "";
|
||||
const { open } = useNotification();
|
||||
@@ -94,100 +97,209 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
// 添加防抖引用
|
||||
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const updateDataStates = useCallback((nodeResults: any[], linkResults: any[]) => {
|
||||
if (setCurrentJunctionCalData) {
|
||||
setCurrentJunctionCalData(nodeResults);
|
||||
} else {
|
||||
console.log("setCurrentJunctionCalData is undefined");
|
||||
}
|
||||
if (setCurrentPipeCalData) {
|
||||
setCurrentPipeCalData(linkResults);
|
||||
} else {
|
||||
console.log("setCurrentPipeCalData is undefined");
|
||||
}
|
||||
}, [setCurrentJunctionCalData, setCurrentPipeCalData]);
|
||||
const updateDataStates = useCallback(
|
||||
(
|
||||
nodeResults: any[],
|
||||
linkResults: any[],
|
||||
target: "primary" | "compare" = "primary"
|
||||
) => {
|
||||
const setNodeData =
|
||||
target === "compare"
|
||||
? setCompareJunctionCalData
|
||||
: setCurrentJunctionCalData;
|
||||
const setLinkData =
|
||||
target === "compare" ? setComparePipeCalData : setCurrentPipeCalData;
|
||||
|
||||
const fetchFrameData = useCallback(async (
|
||||
queryTime: Date,
|
||||
junctionProperties: string,
|
||||
pipeProperties: string,
|
||||
schemeName: string,
|
||||
schemeType: string,
|
||||
) => {
|
||||
const query_time = queryTime.toISOString();
|
||||
let nodeRecords: any = { results: [] };
|
||||
let linkRecords: any = { results: [] };
|
||||
const requests: Promise<Response>[] = [];
|
||||
let nodePromise: Promise<any> | null = null;
|
||||
let linkPromise: Promise<any> | null = null;
|
||||
// 检查node缓存
|
||||
if (junctionProperties !== "" && junctionProperties !== "elevation") {
|
||||
const nodeCacheKey = `${query_time}_${junctionProperties}_${schemeName}_${schemeType}`;
|
||||
if (nodeCacheRef.current.has(nodeCacheKey)) {
|
||||
nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!;
|
||||
} else {
|
||||
disableDateSelection && schemeName
|
||||
? (nodePromise = apiFetch(
|
||||
// `${config.BACKEND_URL}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}&schemename=${schemeName}`
|
||||
`${config.BACKEND_URL}/api/v1/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=node&property=${junctionProperties}`,
|
||||
))
|
||||
: (nodePromise = apiFetch(
|
||||
// `${config.BACKEND_URL}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}`
|
||||
`${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=node&property=${junctionProperties}`,
|
||||
));
|
||||
requests.push(nodePromise);
|
||||
setNodeData?.(nodeResults);
|
||||
setLinkData?.(linkResults);
|
||||
},
|
||||
[
|
||||
setCompareJunctionCalData,
|
||||
setComparePipeCalData,
|
||||
setCurrentJunctionCalData,
|
||||
setCurrentPipeCalData,
|
||||
]
|
||||
);
|
||||
|
||||
const buildCacheKey = useCallback(
|
||||
(
|
||||
queryTime: string,
|
||||
property: string,
|
||||
sourceType: "scheme" | "realtime",
|
||||
resultType: "node" | "link",
|
||||
targetSchemeName: string,
|
||||
targetSchemeType: string
|
||||
) =>
|
||||
[
|
||||
queryTime,
|
||||
sourceType,
|
||||
resultType,
|
||||
property,
|
||||
targetSchemeName || "default",
|
||||
targetSchemeType || "default",
|
||||
].join("::"),
|
||||
[]
|
||||
);
|
||||
|
||||
const fetchDataBySource = useCallback(
|
||||
async ({
|
||||
queryTime,
|
||||
junctionProperties,
|
||||
pipeProperties,
|
||||
sourceType,
|
||||
target,
|
||||
schemeName,
|
||||
schemeType,
|
||||
}: {
|
||||
queryTime: Date;
|
||||
junctionProperties: string;
|
||||
pipeProperties: string;
|
||||
sourceType: "scheme" | "realtime";
|
||||
target: "primary" | "compare";
|
||||
schemeName?: string;
|
||||
schemeType?: string;
|
||||
}) => {
|
||||
const query_time = queryTime.toISOString();
|
||||
let nodeRecords: any = { results: [] };
|
||||
let linkRecords: any = { results: [] };
|
||||
const requests: Promise<Response>[] = [];
|
||||
let nodePromise: Promise<Response> | null = null;
|
||||
let linkPromise: Promise<Response> | null = null;
|
||||
|
||||
if (junctionProperties !== "" && junctionProperties !== "elevation") {
|
||||
const nodeCacheKey = buildCacheKey(
|
||||
query_time,
|
||||
junctionProperties,
|
||||
sourceType,
|
||||
"node",
|
||||
schemeName || "",
|
||||
schemeType || ""
|
||||
);
|
||||
if (nodeCacheRef.current.has(nodeCacheKey)) {
|
||||
nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!;
|
||||
} else {
|
||||
nodePromise =
|
||||
sourceType === "scheme" && schemeName
|
||||
? apiFetch(
|
||||
`${config.BACKEND_URL}/api/v1/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=node&property=${junctionProperties}`
|
||||
)
|
||||
: apiFetch(
|
||||
`${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=node&property=${junctionProperties}`
|
||||
);
|
||||
requests.push(nodePromise);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理特殊属性名称
|
||||
if (pipeProperties === "unit_headloss") pipeProperties = "headloss";
|
||||
|
||||
// 检查link缓存
|
||||
if (pipeProperties !== "" && pipeProperties !== "diameter") {
|
||||
const linkCacheKey = `${query_time}_${pipeProperties}_${schemeName}_${schemeType}`;
|
||||
if (linkCacheRef.current.has(linkCacheKey)) {
|
||||
linkRecords = linkCacheRef.current.get(linkCacheKey)!;
|
||||
} else {
|
||||
disableDateSelection && schemeName
|
||||
? (linkPromise = apiFetch(
|
||||
// `${config.BACKEND_URL}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}&schemename=${schemeName}`
|
||||
`${config.BACKEND_URL}/api/v1/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=link&property=${pipeProperties}`,
|
||||
))
|
||||
: (linkPromise = apiFetch(
|
||||
// `${config.BACKEND_URL}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}`
|
||||
`${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=link&property=${pipeProperties}`,
|
||||
));
|
||||
requests.push(linkPromise);
|
||||
const normalizedPipeProperties =
|
||||
pipeProperties === "unit_headloss" ? "headloss" : pipeProperties;
|
||||
|
||||
if (normalizedPipeProperties !== "" && normalizedPipeProperties !== "diameter") {
|
||||
const linkCacheKey = buildCacheKey(
|
||||
query_time,
|
||||
normalizedPipeProperties,
|
||||
sourceType,
|
||||
"link",
|
||||
schemeName || "",
|
||||
schemeType || ""
|
||||
);
|
||||
if (linkCacheRef.current.has(linkCacheKey)) {
|
||||
linkRecords = linkCacheRef.current.get(linkCacheKey)!;
|
||||
} else {
|
||||
linkPromise =
|
||||
sourceType === "scheme" && schemeName
|
||||
? apiFetch(
|
||||
`${config.BACKEND_URL}/api/v1/scheme/query/by-scheme-time-property?scheme_type=${schemeType}&scheme_name=${schemeName}&query_time=${query_time}&type=link&property=${normalizedPipeProperties}`
|
||||
)
|
||||
: apiFetch(
|
||||
`${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=link&property=${normalizedPipeProperties}`
|
||||
);
|
||||
requests.push(linkPromise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 等待所有有效请求
|
||||
const responses = await Promise.all(requests);
|
||||
const responses = await Promise.all(requests);
|
||||
|
||||
if (nodePromise) {
|
||||
const nodeResponse = responses.shift()!;
|
||||
if (!nodeResponse.ok)
|
||||
throw new Error(`Node fetch failed: ${nodeResponse.status}`);
|
||||
nodeRecords = await nodeResponse.json();
|
||||
// 缓存数据(修复键以包含 schemeName)
|
||||
nodeCacheRef.current.set(
|
||||
`${query_time}_${junctionProperties}_${schemeName}_${schemeType}`,
|
||||
nodeRecords || [],
|
||||
);
|
||||
}
|
||||
if (linkPromise) {
|
||||
const linkResponse = responses.shift()!;
|
||||
if (!linkResponse.ok)
|
||||
throw new Error(`Link fetch failed: ${linkResponse.status}`);
|
||||
linkRecords = await linkResponse.json();
|
||||
// 缓存数据(修复键以包含 schemeName)
|
||||
linkCacheRef.current.set(
|
||||
`${query_time}_${pipeProperties}_${schemeName}_${schemeType}`,
|
||||
linkRecords || [],
|
||||
);
|
||||
}
|
||||
// 更新状态
|
||||
updateDataStates(nodeRecords.results || [], linkRecords.results || []);
|
||||
}, [disableDateSelection, updateDataStates]);
|
||||
if (nodePromise) {
|
||||
const nodeResponse = responses.shift()!;
|
||||
if (!nodeResponse.ok) {
|
||||
throw new Error(`Node fetch failed: ${nodeResponse.status}`);
|
||||
}
|
||||
nodeRecords = await nodeResponse.json();
|
||||
nodeCacheRef.current.set(
|
||||
buildCacheKey(
|
||||
query_time,
|
||||
junctionProperties,
|
||||
sourceType,
|
||||
"node",
|
||||
schemeName || "",
|
||||
schemeType || ""
|
||||
),
|
||||
nodeRecords || []
|
||||
);
|
||||
}
|
||||
|
||||
if (linkPromise) {
|
||||
const linkResponse = responses.shift()!;
|
||||
if (!linkResponse.ok) {
|
||||
throw new Error(`Link fetch failed: ${linkResponse.status}`);
|
||||
}
|
||||
linkRecords = await linkResponse.json();
|
||||
linkCacheRef.current.set(
|
||||
buildCacheKey(
|
||||
query_time,
|
||||
normalizedPipeProperties,
|
||||
sourceType,
|
||||
"link",
|
||||
schemeName || "",
|
||||
schemeType || ""
|
||||
),
|
||||
linkRecords || []
|
||||
);
|
||||
}
|
||||
|
||||
updateDataStates(nodeRecords.results || [], linkRecords.results || [], target);
|
||||
},
|
||||
[buildCacheKey, updateDataStates]
|
||||
);
|
||||
|
||||
const fetchFrameData = useCallback(
|
||||
async (
|
||||
queryTime: Date,
|
||||
junctionProperties: string,
|
||||
pipeProperties: string,
|
||||
schemeName: string,
|
||||
schemeType: string
|
||||
) => {
|
||||
const primarySourceType =
|
||||
disableDateSelection && schemeName ? "scheme" : "realtime";
|
||||
const tasks = [
|
||||
fetchDataBySource({
|
||||
queryTime,
|
||||
junctionProperties,
|
||||
pipeProperties,
|
||||
sourceType: primarySourceType,
|
||||
target: "primary",
|
||||
schemeName,
|
||||
schemeType,
|
||||
}),
|
||||
];
|
||||
|
||||
if (isCompareMode && disableDateSelection && schemeName) {
|
||||
tasks.push(
|
||||
fetchDataBySource({
|
||||
queryTime,
|
||||
junctionProperties,
|
||||
pipeProperties,
|
||||
sourceType: "realtime",
|
||||
target: "compare",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
},
|
||||
[disableDateSelection, fetchDataBySource, isCompareMode]
|
||||
);
|
||||
|
||||
// 时间刻度数组 (每5分钟一个刻度)
|
||||
const timeMarks = Array.from({ length: 288 }, (_, i) => ({
|
||||
@@ -453,9 +565,9 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
if (!cacheRef.current) return;
|
||||
const cacheKeys = Array.from(cacheRef.current.keys());
|
||||
cacheKeys.forEach((key) => {
|
||||
const keyParts = key.split("_");
|
||||
const cacheDate = keyParts[0].split("T")[0];
|
||||
const cacheTimeStr = keyParts[0].split("T")[1];
|
||||
const cacheTimeKey = key.split("::")[0];
|
||||
const cacheDate = cacheTimeKey.split("T")[0];
|
||||
const cacheTimeStr = cacheTimeKey.split("T")[1];
|
||||
|
||||
if (cacheDate === dateStr && cacheTimeStr) {
|
||||
const [hours, minutes] = cacheTimeStr.split(":");
|
||||
|
||||
@@ -5,6 +5,7 @@ import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
||||
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||
import PaletteOutlinedIcon from "@mui/icons-material/PaletteOutlined";
|
||||
import QueryStatsOutlinedIcon from "@mui/icons-material/QueryStatsOutlined";
|
||||
import CompareArrowsOutlinedIcon from "@mui/icons-material/CompareArrowsOutlined";
|
||||
import PropertyPanel from "./PropertyPanel"; // 引入属性面板组件
|
||||
import DrawPanel from "./DrawPanel"; // 引入绘图面板组件
|
||||
import HistoryDataPanel from "./HistoryDataPanel"; // 引入绘图面板组件
|
||||
@@ -34,12 +35,14 @@ interface ToolbarProps {
|
||||
queryType?: string; // 可选的查询类型参数
|
||||
schemeType?: string; // 可选的方案类型参数
|
||||
HistoryPanel?: React.FC<any>; // 可选的自定义历史数据面板
|
||||
enableCompare?: boolean;
|
||||
}
|
||||
const Toolbar: React.FC<ToolbarProps> = ({
|
||||
hiddenButtons,
|
||||
queryType,
|
||||
schemeType,
|
||||
HistoryPanel,
|
||||
enableCompare = false,
|
||||
}) => {
|
||||
const map = useMap();
|
||||
const data = useData();
|
||||
@@ -55,6 +58,17 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
const currentTime = data?.currentTime;
|
||||
const selectedDate = data?.selectedDate;
|
||||
const schemeName = data?.schemeName;
|
||||
const isCompareMode = data?.isCompareMode ?? false;
|
||||
const toggleCompareMode = data?.toggleCompareMode;
|
||||
const canToggleCompare = Boolean(
|
||||
enableCompare && (isCompareMode || (queryType === "scheme" && schemeName)),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableCompare && isCompareMode) {
|
||||
toggleCompareMode?.();
|
||||
}
|
||||
}, [enableCompare, isCompareMode, toggleCompareMode]);
|
||||
|
||||
// Chat tool action → direct featureInfos override (bypasses OL Feature lookup)
|
||||
const [chatPanelFeatureInfos, setChatPanelFeatureInfos] = useState<
|
||||
@@ -853,6 +867,15 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
onClick={() => handleToolClick("style")}
|
||||
/>
|
||||
)}
|
||||
{enableCompare && (
|
||||
<ToolbarButton
|
||||
icon={<CompareArrowsOutlinedIcon />}
|
||||
name={isCompareMode ? "关闭对比" : "双屏对比"}
|
||||
isActive={isCompareMode}
|
||||
onClick={() => toggleCompareMode?.()}
|
||||
disabled={!canToggleCompare}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showPropertyPanel && <PropertyPanel {...getFeatureProperties()} />}
|
||||
{showDrawPanel && map && <DrawPanel />}
|
||||
|
||||
@@ -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<React.SetStateAction<any[]>>;
|
||||
currentPipeCalData?: any[]; // 当前计算结果
|
||||
setCurrentPipeCalData?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
compareJunctionCalData?: any[];
|
||||
setCompareJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
comparePipeCalData?: any[];
|
||||
setComparePipeCalData?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
isCompareMode?: boolean;
|
||||
setCompareMode?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
toggleCompareMode?: () => void;
|
||||
showJunctionText?: boolean; // 是否显示节点文本
|
||||
showPipeText?: boolean; // 是否显示管道文本
|
||||
showJunctionId?: boolean; // 是否显示节点ID
|
||||
@@ -69,6 +77,10 @@ interface DataContextType {
|
||||
setPipeText?: React.Dispatch<React.SetStateAction<string>>;
|
||||
setContours?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
deckLayer?: DeckLayer;
|
||||
compareDeckLayer?: DeckLayer;
|
||||
deckLayers?: DeckLayer[];
|
||||
compareMap?: OlMap;
|
||||
maps?: OlMap[];
|
||||
diameterRange?: [number, number];
|
||||
elevationRange?: [number, number];
|
||||
}
|
||||
@@ -128,12 +140,18 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
|
||||
const mapRef = useRef<HTMLDivElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const compareMapRef = useRef<HTMLDivElement | null>(null);
|
||||
const compareCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const deckLayerRef = useRef<DeckLayer | null>(null);
|
||||
const compareDeckLayerRef = useRef<DeckLayer | null>(null);
|
||||
const isDisposingRef = useRef(false);
|
||||
const isCompareDisposingRef = useRef(false);
|
||||
const pendingTimeoutsRef = useRef<number[]>([]);
|
||||
|
||||
const [map, setMap] = useState<OlMap>();
|
||||
const [deckLayer, setDeckLayer] = useState<DeckLayer>();
|
||||
const [compareMap, setCompareMap] = useState<OlMap>();
|
||||
const [compareDeckLayer, setCompareDeckLayer] = useState<DeckLayer>();
|
||||
// currentCalData 用于存储当前计算结果
|
||||
const [currentTime, setCurrentTime] = useState<number>(-1); // 默认选择当前时间
|
||||
// const [selectedDate, setSelectedDate] = useState<Date>(new Date("2025-9-17"));
|
||||
@@ -144,6 +162,11 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
[],
|
||||
);
|
||||
const [currentPipeCalData, setCurrentPipeCalData] = useState<any[]>([]);
|
||||
const [compareJunctionCalData, setCompareJunctionCalData] = useState<any[]>(
|
||||
[],
|
||||
);
|
||||
const [comparePipeCalData, setComparePipeCalData] = useState<any[]>([]);
|
||||
const [isCompareMode, setCompareMode] = useState(false);
|
||||
// junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染
|
||||
// currentJunctionCalData 和 currentPipeCalData 变化时会新增并更新 junctionData 和 pipeData 的计算属性值
|
||||
const [junctionData, setJunctionDataState] = useState<any[]>([]);
|
||||
@@ -201,6 +224,37 @@ const MapComponent: React.FC<MapComponentProps> = ({ 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<MapComponentProps> = ({ 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<MapComponentProps> = ({ 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<MapComponentProps> = ({ 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<MapComponentProps> = ({ 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<MapComponentProps> = ({ children }) => {
|
||||
currentPipeCalData,
|
||||
currentZoom,
|
||||
mergedPipeData,
|
||||
mergedComparePipeData,
|
||||
isCompareMode,
|
||||
pipeText,
|
||||
isWaterflowLayerAvailable,
|
||||
showWaterflowLayer,
|
||||
@@ -1097,6 +1491,13 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
setCurrentJunctionCalData,
|
||||
currentPipeCalData,
|
||||
setCurrentPipeCalData,
|
||||
compareJunctionCalData,
|
||||
setCompareJunctionCalData,
|
||||
comparePipeCalData,
|
||||
setComparePipeCalData,
|
||||
isCompareMode,
|
||||
setCompareMode,
|
||||
toggleCompareMode,
|
||||
setShowJunctionTextLayer,
|
||||
setShowPipeTextLayer,
|
||||
setShowJunctionId,
|
||||
@@ -1115,17 +1516,50 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
pipeText,
|
||||
setContours,
|
||||
deckLayer,
|
||||
compareDeckLayer,
|
||||
deckLayers,
|
||||
compareMap,
|
||||
maps,
|
||||
diameterRange,
|
||||
elevationRange,
|
||||
}}
|
||||
>
|
||||
<MapContext.Provider value={map}>
|
||||
<div className="relative w-full h-full">
|
||||
<div ref={mapRef} className="w-full h-full"></div>
|
||||
<div className="flex w-full h-full">
|
||||
<div
|
||||
className={`relative h-full ${isCompareMode ? "w-1/2" : "w-full"}`}
|
||||
>
|
||||
<div ref={mapRef} className="w-full h-full"></div>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="pointer-events-none absolute inset-0"
|
||||
/>
|
||||
{isCompareMode && (
|
||||
<div className="pointer-events-none absolute left-4 top-4 rounded-md bg-black/55 px-3 py-1 text-sm font-medium text-white">
|
||||
方案模拟
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCompareMode && (
|
||||
<div className="relative h-full w-1/2 border-l border-white/40">
|
||||
<div ref={compareMapRef} className="w-full h-full"></div>
|
||||
<canvas
|
||||
ref={compareCanvasRef}
|
||||
className="pointer-events-none absolute inset-0"
|
||||
/>
|
||||
<div className="pointer-events-none absolute left-4 top-4 rounded-md bg-black/55 px-3 py-1 text-sm font-medium text-white">
|
||||
实时模拟
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCompareMode && (
|
||||
<div className="pointer-events-none absolute inset-y-0 left-1/2 z-10 w-px -translate-x-1/2 bg-white/85 shadow-[0_0_0_1px_rgba(15,23,42,0.18)]" />
|
||||
)}
|
||||
<MapTools />
|
||||
{children}
|
||||
</div>
|
||||
<canvas ref={canvasRef} />
|
||||
</MapContext.Provider>
|
||||
</DataContext.Provider>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user