添加对比模式功能,优化地图组件
This commit is contained in:
@@ -8,7 +8,11 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full overflow-hidden">
|
<div className="relative w-full h-full overflow-hidden">
|
||||||
<MapComponent>
|
<MapComponent>
|
||||||
<MapToolbar queryType="scheme" schemeType="burst_analysis" />
|
<MapToolbar
|
||||||
|
queryType="scheme"
|
||||||
|
schemeType="burst_analysis"
|
||||||
|
enableCompare
|
||||||
|
/>
|
||||||
<BurstPipeAnalysisPanel />
|
<BurstPipeAnalysisPanel />
|
||||||
</MapComponent>
|
</MapComponent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full overflow-hidden">
|
<div className="relative w-full h-full overflow-hidden">
|
||||||
<MapComponent>
|
<MapComponent>
|
||||||
<MapToolbar queryType="scheme" schemeType="contaminant_analysis" />
|
<MapToolbar
|
||||||
|
queryType="scheme"
|
||||||
|
schemeType="contaminant_analysis"
|
||||||
|
enableCompare
|
||||||
|
/>
|
||||||
<WaterQualityPanel />
|
<WaterQualityPanel />
|
||||||
</MapComponent>
|
</MapComponent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,107 +1,91 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useMemo, useRef } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useMap } from "../MapComponent";
|
import { useData, useMap } from "../MapComponent";
|
||||||
import TileLayer from "ol/layer/Tile.js";
|
import TileLayer from "ol/layer/Tile.js";
|
||||||
import XYZ from "ol/source/XYZ.js";
|
import XYZ from "ol/source/XYZ.js";
|
||||||
|
import Group from "ol/layer/Group";
|
||||||
import mapboxOutdoors from "@assets/map/layers/mapbox-outdoors.png";
|
import mapboxOutdoors from "@assets/map/layers/mapbox-outdoors.png";
|
||||||
import mapboxLight from "@assets/map/layers/mapbox-light.png";
|
import mapboxLight from "@assets/map/layers/mapbox-light.png";
|
||||||
import mapboxSatellite from "@assets/map/layers/mapbox-satellite.png";
|
import mapboxSatellite from "@assets/map/layers/mapbox-satellite.png";
|
||||||
import mapboxSatelliteStreet from "@assets/map/layers/mapbox-satellite-streets.png";
|
import mapboxSatelliteStreet from "@assets/map/layers/mapbox-satellite-streets.png";
|
||||||
import mapboxStreets from "@assets/map/layers/mapbox-streets.png";
|
import mapboxStreets from "@assets/map/layers/mapbox-streets.png";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Group from "ol/layer/Group";
|
import { MAPBOX_TOKEN, TIANDITU_TOKEN } from "@config/config";
|
||||||
import { MAPBOX_TOKEN } from "@config/config";
|
import type { Map as OlMap } from "ol";
|
||||||
import { TIANDITU_TOKEN } from "@config/config";
|
|
||||||
const INITIAL_LAYER = "mapbox-light";
|
const INITIAL_LAYER = "mapbox-light";
|
||||||
|
|
||||||
const streetsLayer = new TileLayer({
|
const createTileLayer = (url: string, attributions: string) =>
|
||||||
|
new TileLayer({
|
||||||
source: new XYZ({
|
source: new XYZ({
|
||||||
url: `https://api.mapbox.com/styles/v1/mapbox/streets-v12/tiles/256/{z}/{x}/{y}@2x?access_token=${MAPBOX_TOKEN}`,
|
url,
|
||||||
tileSize: 512,
|
tileSize: 512,
|
||||||
maxZoom: 20,
|
maxZoom: 20,
|
||||||
projection: "EPSG:3857",
|
projection: "EPSG:3857",
|
||||||
attributions:
|
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 tiandituVectorLayer = new TileLayer({
|
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({
|
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}`,
|
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",
|
projection: "EPSG:3857",
|
||||||
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const tiandituVectorAnnotationLayer = new TileLayer({
|
const tiandituVectorAnnotationLayer = new TileLayer({
|
||||||
source: new XYZ({
|
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}`,
|
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",
|
projection: "EPSG:3857",
|
||||||
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const tiandituImageLayer = new TileLayer({
|
const tiandituImageLayer = new TileLayer({
|
||||||
source: new XYZ({
|
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}`,
|
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",
|
projection: "EPSG:3857",
|
||||||
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const tiandituImageAnnotationLayer = new TileLayer({
|
const tiandituImageAnnotationLayer = new TileLayer({
|
||||||
source: new XYZ({
|
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}`,
|
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",
|
projection: "EPSG:3857",
|
||||||
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
attributions: '数据来源:<a href="https://www.tianditu.gov.cn/">天地图</a>',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const tiandituVectorLayerGroup = new Group({
|
|
||||||
layers: [tiandituVectorLayer, tiandituVectorAnnotationLayer],
|
return [
|
||||||
});
|
|
||||||
const tiandituImageLayerGroup = new Group({
|
|
||||||
layers: [tiandituImageLayer, tiandituImageAnnotationLayer],
|
|
||||||
});
|
|
||||||
const baseLayers = [
|
|
||||||
{
|
{
|
||||||
id: "mapbox-light",
|
id: "mapbox-light",
|
||||||
name: "默认地图",
|
name: "默认地图",
|
||||||
layer: lightMapLayer,
|
layer: lightMapLayer,
|
||||||
// layer: tiandituVectorLayerGroup,
|
|
||||||
img: mapboxLight.src,
|
img: mapboxLight.src,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "mapbox-satellite",
|
id: "mapbox-satellite",
|
||||||
name: "卫星地图",
|
name: "卫星地图",
|
||||||
layer: satelliteLayer,
|
layer: satelliteLayer,
|
||||||
// layer: tiandituImageLayerGroup,
|
|
||||||
img: mapboxSatellite.src,
|
img: mapboxSatellite.src,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -116,43 +100,75 @@ const baseLayers = [
|
|||||||
layer: streetsLayer,
|
layer: streetsLayer,
|
||||||
img: mapboxStreets.src,
|
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 BaseLayers: React.FC = () => {
|
||||||
const map = useMap();
|
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 [isShow, setShow] = useState(false);
|
||||||
const [isExpanded, setExpanded] = useState(false);
|
const [isExpanded, setExpanded] = useState(false);
|
||||||
// 快速切换底图
|
|
||||||
const [activeId, setActiveId] = useState(INITIAL_LAYER);
|
const [activeId, setActiveId] = useState(INITIAL_LAYER);
|
||||||
|
|
||||||
// 初始化默认底图
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map) return;
|
maps.forEach((targetMap) => {
|
||||||
// 添加所有底图至地图并根据 activeId 控制可见性
|
let layerEntries = layerSetsRef.current.get(targetMap);
|
||||||
baseLayers.forEach((layerInfo) => {
|
if (!layerEntries) {
|
||||||
const layers = map.getLayers().getArray();
|
layerEntries = createBaseLayerEntries();
|
||||||
|
layerSetsRef.current.set(targetMap, layerEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
layerEntries.forEach((layerInfo) => {
|
||||||
|
const layers = targetMap.getLayers().getArray();
|
||||||
if (!layers.includes(layerInfo.layer)) {
|
if (!layers.includes(layerInfo.layer)) {
|
||||||
map.getLayers().insertAt(0, layerInfo.layer);
|
targetMap.getLayers().insertAt(0, layerInfo.layer);
|
||||||
}
|
}
|
||||||
layerInfo.layer.setVisible(layerInfo.id === activeId);
|
layerInfo.layer.setVisible(layerInfo.id === activeId);
|
||||||
});
|
});
|
||||||
}, [map, activeId]);
|
});
|
||||||
|
}, [activeId, maps]);
|
||||||
|
|
||||||
const changeMapLayers = (id: string) => {
|
const changeMapLayers = (id: string) => {
|
||||||
if (map) {
|
maps.forEach((targetMap) => {
|
||||||
// 根据 id 设置每个图层的可见性
|
const layerEntries = layerSetsRef.current.get(targetMap);
|
||||||
baseLayers.forEach(({ id: lid, layer }) => {
|
layerEntries?.forEach(({ id: layerId, layer }) => {
|
||||||
layer.setVisible(lid === id);
|
layer.setVisible(layerId === id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const baseLayers = useMemo(() => createBaseLayerEntries().map(({ id, name, img }) => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
img,
|
||||||
|
})), []);
|
||||||
|
|
||||||
const handleQuickSwitch = () => {
|
const handleQuickSwitch = () => {
|
||||||
const nextId =
|
const nextId =
|
||||||
activeId === baseLayers[0].id ? baseLayers[1].id : baseLayers[0].id;
|
activeId === baseLayers[0].id ? baseLayers[1].id : baseLayers[0].id;
|
||||||
setActiveId(nextId);
|
setActiveId(nextId);
|
||||||
handleMapLayers(nextId);
|
changeMapLayers(nextId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMapLayers = (id: string) => {
|
const handleMapLayers = (id: string) => {
|
||||||
@@ -160,7 +176,6 @@ const BaseLayers: React.FC = () => {
|
|||||||
changeMapLayers(id);
|
changeMapLayers(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 记录定时器,避免多次触发
|
|
||||||
const hideTimer = React.useRef<NodeJS.Timeout | null>(null);
|
const hideTimer = React.useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const handleEnter = () => {
|
const handleEnter = () => {
|
||||||
@@ -217,7 +232,7 @@ const BaseLayers: React.FC = () => {
|
|||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
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"
|
isShow ? "opacity-100" : "opacity-0"
|
||||||
)}
|
)}
|
||||||
onMouseEnter={handleEnter}
|
onMouseEnter={handleEnter}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
|||||||
import VectorLayer from "ol/layer/Vector";
|
import VectorLayer from "ol/layer/Vector";
|
||||||
import VectorTileLayer from "ol/layer/VectorTile";
|
import VectorTileLayer from "ol/layer/VectorTile";
|
||||||
import { DeckLayer } from "@utils/layers";
|
import { DeckLayer } from "@utils/layers";
|
||||||
|
import type { Map as OlMap } from "ol";
|
||||||
|
|
||||||
// 定义统一的图层项接口
|
// 定义统一的图层项接口
|
||||||
interface LayerItem {
|
interface LayerItem {
|
||||||
@@ -30,8 +31,10 @@ const LAYER_ORDER = [
|
|||||||
const LayerControl: React.FC = () => {
|
const LayerControl: React.FC = () => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const data = useData();
|
const data = useData();
|
||||||
|
const maps: OlMap[] = data?.maps?.length ? data.maps : map ? [map] : [];
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
const deckLayer = data?.deckLayer;
|
const deckLayer = data?.deckLayer;
|
||||||
|
const deckLayers = data?.deckLayers ?? (deckLayer ? [deckLayer] : []);
|
||||||
const isContourLayerAvailable = data?.isContourLayerAvailable;
|
const isContourLayerAvailable = data?.isContourLayerAvailable;
|
||||||
const isWaterflowLayerAvailable = data?.isWaterflowLayerAvailable;
|
const isWaterflowLayerAvailable = data?.isWaterflowLayerAvailable;
|
||||||
const setShowWaterflowLayer = data?.setShowWaterflowLayer;
|
const setShowWaterflowLayer = data?.setShowWaterflowLayer;
|
||||||
@@ -117,8 +120,16 @@ const LayerControl: React.FC = () => {
|
|||||||
|
|
||||||
const handleVisibilityChange = (item: LayerItem, checked: boolean) => {
|
const handleVisibilityChange = (item: LayerItem, checked: boolean) => {
|
||||||
if (item.type === "ol") {
|
if (item.type === "ol") {
|
||||||
item.layerRef.setVisible(checked);
|
maps.forEach((targetMap) => {
|
||||||
} else if (item.type === "deck" && deckLayer) {
|
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") {
|
if (item.id === "junctionContourLayer") {
|
||||||
setShowContourLayer && setShowContourLayer(checked);
|
setShowContourLayer && setShowContourLayer(checked);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { FlatStyleLike } from "ol/style/flat";
|
|||||||
import { calculateClassification } from "@utils/breaks_classification";
|
import { calculateClassification } from "@utils/breaks_classification";
|
||||||
import { parseColor } from "@utils/parseColor";
|
import { parseColor } from "@utils/parseColor";
|
||||||
import { VectorTile } from "ol";
|
import { VectorTile } from "ol";
|
||||||
|
import type { Map as OlMap } from "ol";
|
||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
import { config } from "@/config/config";
|
import { config } from "@/config/config";
|
||||||
|
|
||||||
@@ -182,6 +183,13 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
const data = useData();
|
const data = useData();
|
||||||
const currentJunctionCalData = data?.currentJunctionCalData;
|
const currentJunctionCalData = data?.currentJunctionCalData;
|
||||||
const currentPipeCalData = data?.currentPipeCalData;
|
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 junctionText = data?.junctionText ?? "";
|
||||||
const pipeText = data?.pipeText ?? "";
|
const pipeText = data?.pipeText ?? "";
|
||||||
const setShowJunctionTextLayer = data?.setShowJunctionTextLayer;
|
const setShowJunctionTextLayer = data?.setShowJunctionTextLayer;
|
||||||
@@ -229,6 +237,45 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
customColors: [],
|
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 = (
|
const getDefaultCustomColors = (
|
||||||
segments: number,
|
segments: number,
|
||||||
existingColors: string[] = []
|
existingColors: string[] = []
|
||||||
@@ -613,13 +660,10 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const styleConfig = layerStyleConfig.styleConfig;
|
const styleConfig = layerStyleConfig.styleConfig;
|
||||||
const renderLayer = renderLayers.filter((layer) => {
|
const targetLayers = getRenderLayersById(layerStyleConfig.layerId);
|
||||||
return layer.get("value") === layerStyleConfig.layerId;
|
const renderLayer = targetLayers[0];
|
||||||
})[0];
|
|
||||||
if (!renderLayer || !styleConfig?.property) return;
|
if (!renderLayer || !styleConfig?.property) return;
|
||||||
const layerType: string = renderLayer?.get("type");
|
const layerType: string = renderLayer.get("type");
|
||||||
const source = renderLayer.getSource();
|
|
||||||
if (!source) return;
|
|
||||||
|
|
||||||
const breaksLength = breaks.length;
|
const breaksLength = breaks.length;
|
||||||
// 根据 breaks 计算每个分段的颜色,线条粗细
|
// 根据 breaks 计算每个分段的颜色,线条粗细
|
||||||
@@ -757,7 +801,9 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
dynamicStyle["circle-stroke-width"] = 2;
|
dynamicStyle["circle-stroke-width"] = 2;
|
||||||
}
|
}
|
||||||
// 应用样式到图层
|
// 应用样式到图层
|
||||||
renderLayer.setStyle(dynamicStyle);
|
targetLayers.forEach((targetLayer) => {
|
||||||
|
targetLayer.setStyle(dynamicStyle);
|
||||||
|
});
|
||||||
// 用初始化时的样式配置更新图例配置,避免覆盖已有的图例名称和属性
|
// 用初始化时的样式配置更新图例配置,避免覆盖已有的图例名称和属性
|
||||||
const layerId = renderLayer.get("value");
|
const layerId = renderLayer.get("value");
|
||||||
const initLayerStyleState = layerStyleStates.find(
|
const initLayerStyleState = layerStyleStates.find(
|
||||||
@@ -844,10 +890,12 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
if (!selectedRenderLayer) return;
|
if (!selectedRenderLayer) return;
|
||||||
// 重置 WebGL 图层样式
|
// 重置 WebGL 图层样式
|
||||||
const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE;
|
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) {
|
if (layerId !== undefined) {
|
||||||
setLayerStyleStates((prev) =>
|
setLayerStyleStates((prev) =>
|
||||||
prev.filter((state) => state.layerId !== layerId)
|
prev.filter((state) => state.layerId !== layerId)
|
||||||
@@ -870,11 +918,15 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 更新当前 VectorTileSource 中的所有缓冲要素属性
|
// 更新当前 VectorTileSource 中的所有缓冲要素属性
|
||||||
const updateVectorTileSource = (property: string, data: any[]) => {
|
const updateVectorTileSource = (
|
||||||
if (!map) return;
|
targetMap: OlMap,
|
||||||
const vectorTileSources = map
|
layerId: string,
|
||||||
|
property: string,
|
||||||
|
data: any[]
|
||||||
|
) => {
|
||||||
|
const vectorTileSources = targetMap
|
||||||
.getAllLayers()
|
.getAllLayers()
|
||||||
.filter((layer) => layer instanceof WebGLVectorTileLayer)
|
.filter((layer) => layer.get("value") === layerId)
|
||||||
.map((layer) => layer.getSource() as VectorTileSource)
|
.map((layer) => layer.getSource() as VectorTileSource)
|
||||||
.filter((source) => source);
|
.filter((source) => source);
|
||||||
|
|
||||||
@@ -911,16 +963,16 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
};
|
};
|
||||||
// 新增事件,监听 VectorTileSource 的 tileloadend 事件,为新增瓦片数据动态更新要素属性
|
// 新增事件,监听 VectorTileSource 的 tileloadend 事件,为新增瓦片数据动态更新要素属性
|
||||||
const tileLoadListenersRef = useRef<
|
const tileLoadListenersRef = useRef<
|
||||||
Map<VectorTileSource, (event: any) => void>
|
Map<string, { source: VectorTileSource; listener: (event: any) => void }>
|
||||||
>(new Map());
|
>(new Map());
|
||||||
|
|
||||||
const attachVectorTileSourceLoadedEvent = (
|
const attachVectorTileSourceLoadedEvent = (
|
||||||
|
targetMap: OlMap,
|
||||||
layerId: string,
|
layerId: string,
|
||||||
property: string,
|
property: string,
|
||||||
data: any[]
|
data: any[]
|
||||||
) => {
|
) => {
|
||||||
if (!map) return;
|
const vectorTileSource = targetMap
|
||||||
const vectorTileSource = map
|
|
||||||
.getAllLayers()
|
.getAllLayers()
|
||||||
.filter((layer) => layer.get("value") === layerId)
|
.filter((layer) => layer.get("value") === layerId)
|
||||||
.map((layer) => layer.getSource() as VectorTileSource)
|
.map((layer) => layer.getSource() as VectorTileSource)
|
||||||
@@ -956,24 +1008,25 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listenerKey = getMapKey(targetMap, layerId);
|
||||||
vectorTileSource.on("tileloadend", listener);
|
vectorTileSource.on("tileloadend", listener);
|
||||||
tileLoadListenersRef.current.set(vectorTileSource, listener);
|
tileLoadListenersRef.current.set(listenerKey, {
|
||||||
|
source: vectorTileSource,
|
||||||
|
listener,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
// 新增函数:取消对应 layerId 已添加的 on 事件
|
// 新增函数:取消对应 layerId 已添加的 on 事件
|
||||||
const removeVectorTileSourceLoadedEvent = (layerId: string) => {
|
const removeVectorTileSourceLoadedEvent = useCallback(
|
||||||
if (!map) return;
|
(targetMap: OlMap, layerId: string) => {
|
||||||
const vectorTileSource = map
|
const listenerKey = getMapKey(targetMap, layerId);
|
||||||
.getAllLayers()
|
const listenerState = tileLoadListenersRef.current.get(listenerKey);
|
||||||
.filter((layer) => layer.get("value") === layerId)
|
if (listenerState) {
|
||||||
.map((layer) => layer.getSource() as VectorTileSource)
|
listenerState.source.un("tileloadend", listenerState.listener);
|
||||||
.filter((source) => source)[0];
|
tileLoadListenersRef.current.delete(listenerKey);
|
||||||
if (!vectorTileSource) return;
|
|
||||||
const listener = tileLoadListenersRef.current.get(vectorTileSource);
|
|
||||||
if (listener) {
|
|
||||||
vectorTileSource.un("tileloadend", listener);
|
|
||||||
tileLoadListenersRef.current.delete(vectorTileSource);
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[getMapKey]
|
||||||
|
);
|
||||||
|
|
||||||
// 监听数据变化,重新应用样式。由样式应用按钮触发,或由数据变化触发
|
// 监听数据变化,重新应用样式。由样式应用按钮触发,或由数据变化触发
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -998,20 +1051,24 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isElevation) {
|
if (isElevation) {
|
||||||
removeVectorTileSourceLoadedEvent("junctions");
|
activeMaps.forEach((targetMap) => {
|
||||||
|
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentJunctionCalData) return;
|
activeMaps.forEach((targetMap) => {
|
||||||
// 更新现有的 VectorTileSource
|
const targetData = getDataForMap(targetMap, "junctions");
|
||||||
updateVectorTileSource(junctionText, currentJunctionCalData);
|
if (!targetData || targetData.length === 0) return;
|
||||||
// 移除旧的监听器,并添加新的监听器
|
updateVectorTileSource(targetMap, "junctions", junctionText, targetData);
|
||||||
removeVectorTileSourceLoadedEvent("junctions");
|
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||||
attachVectorTileSourceLoadedEvent(
|
attachVectorTileSourceLoadedEvent(
|
||||||
|
targetMap,
|
||||||
"junctions",
|
"junctions",
|
||||||
junctionText,
|
junctionText,
|
||||||
currentJunctionCalData
|
targetData
|
||||||
);
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const updatePipeStyle = () => {
|
const updatePipeStyle = () => {
|
||||||
const pipeStyleConfigState = layerStyleStates.find(
|
const pipeStyleConfigState = layerStyleStates.find(
|
||||||
@@ -1023,16 +1080,24 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
applyClassificationStyle("pipes", pipeStyleConfigState?.styleConfig);
|
applyClassificationStyle("pipes", pipeStyleConfigState?.styleConfig);
|
||||||
|
|
||||||
if (isDiameter) {
|
if (isDiameter) {
|
||||||
removeVectorTileSourceLoadedEvent("pipes");
|
activeMaps.forEach((targetMap) => {
|
||||||
|
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentPipeCalData) return;
|
activeMaps.forEach((targetMap) => {
|
||||||
// 更新现有的 VectorTileSource
|
const targetData = getDataForMap(targetMap, "pipes");
|
||||||
updateVectorTileSource(pipeText, currentPipeCalData);
|
if (!targetData || targetData.length === 0) return;
|
||||||
// 移除旧的监听器,并添加新的监听器
|
updateVectorTileSource(targetMap, "pipes", pipeText, targetData);
|
||||||
removeVectorTileSourceLoadedEvent("pipes");
|
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||||
attachVectorTileSourceLoadedEvent("pipes", pipeText, currentPipeCalData);
|
attachVectorTileSourceLoadedEvent(
|
||||||
|
targetMap,
|
||||||
|
"pipes",
|
||||||
|
pipeText,
|
||||||
|
targetData
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
if (isUserTrigger) {
|
if (isUserTrigger) {
|
||||||
if (selectedRenderLayer?.get("value") === "junctions") {
|
if (selectedRenderLayer?.get("value") === "junctions") {
|
||||||
@@ -1060,10 +1125,14 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
updatePipeStyle();
|
updatePipeStyle();
|
||||||
}
|
}
|
||||||
if (!applyJunctionStyle) {
|
if (!applyJunctionStyle) {
|
||||||
removeVectorTileSourceLoadedEvent("junctions");
|
activeMaps.forEach((targetMap) => {
|
||||||
|
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!applyPipeStyle) {
|
if (!applyPipeStyle) {
|
||||||
removeVectorTileSourceLoadedEvent("pipes");
|
activeMaps.forEach((targetMap) => {
|
||||||
|
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// This effect is intentionally driven by explicit style triggers and data snapshots.
|
// This effect is intentionally driven by explicit style triggers and data snapshots.
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -1073,8 +1142,20 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
applyPipeStyle,
|
applyPipeStyle,
|
||||||
currentJunctionCalData,
|
currentJunctionCalData,
|
||||||
currentPipeCalData,
|
currentPipeCalData,
|
||||||
|
compareJunctionCalData,
|
||||||
|
comparePipeCalData,
|
||||||
|
activeMaps,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
activeMaps.forEach((targetMap) => {
|
||||||
|
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||||
|
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [activeMaps, removeVectorTileSourceLoadedEvent]);
|
||||||
|
|
||||||
// 获取地图中的矢量图层,用于选择图层选项
|
// 获取地图中的矢量图层,用于选择图层选项
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ const Timeline: React.FC<TimelineProps> = ({
|
|||||||
const setSelectedDate = data?.setSelectedDate ?? NOOP_SET_SELECTED_DATE;
|
const setSelectedDate = data?.setSelectedDate ?? NOOP_SET_SELECTED_DATE;
|
||||||
const setCurrentJunctionCalData = data?.setCurrentJunctionCalData;
|
const setCurrentJunctionCalData = data?.setCurrentJunctionCalData;
|
||||||
const setCurrentPipeCalData = data?.setCurrentPipeCalData;
|
const setCurrentPipeCalData = data?.setCurrentPipeCalData;
|
||||||
|
const setCompareJunctionCalData = data?.setCompareJunctionCalData;
|
||||||
|
const setComparePipeCalData = data?.setComparePipeCalData;
|
||||||
|
const isCompareMode = data?.isCompareMode ?? false;
|
||||||
const junctionText = data?.junctionText ?? "";
|
const junctionText = data?.junctionText ?? "";
|
||||||
const pipeText = data?.pipeText ?? "";
|
const pipeText = data?.pipeText ?? "";
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
@@ -94,100 +97,209 @@ const Timeline: React.FC<TimelineProps> = ({
|
|||||||
// 添加防抖引用
|
// 添加防抖引用
|
||||||
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const updateDataStates = useCallback((nodeResults: any[], linkResults: any[]) => {
|
const updateDataStates = useCallback(
|
||||||
if (setCurrentJunctionCalData) {
|
(
|
||||||
setCurrentJunctionCalData(nodeResults);
|
nodeResults: any[],
|
||||||
} else {
|
linkResults: any[],
|
||||||
console.log("setCurrentJunctionCalData is undefined");
|
target: "primary" | "compare" = "primary"
|
||||||
}
|
|
||||||
if (setCurrentPipeCalData) {
|
|
||||||
setCurrentPipeCalData(linkResults);
|
|
||||||
} else {
|
|
||||||
console.log("setCurrentPipeCalData is undefined");
|
|
||||||
}
|
|
||||||
}, [setCurrentJunctionCalData, setCurrentPipeCalData]);
|
|
||||||
|
|
||||||
const fetchFrameData = useCallback(async (
|
|
||||||
queryTime: Date,
|
|
||||||
junctionProperties: string,
|
|
||||||
pipeProperties: string,
|
|
||||||
schemeName: string,
|
|
||||||
schemeType: string,
|
|
||||||
) => {
|
) => {
|
||||||
|
const setNodeData =
|
||||||
|
target === "compare"
|
||||||
|
? setCompareJunctionCalData
|
||||||
|
: setCurrentJunctionCalData;
|
||||||
|
const setLinkData =
|
||||||
|
target === "compare" ? setComparePipeCalData : setCurrentPipeCalData;
|
||||||
|
|
||||||
|
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();
|
const query_time = queryTime.toISOString();
|
||||||
let nodeRecords: any = { results: [] };
|
let nodeRecords: any = { results: [] };
|
||||||
let linkRecords: any = { results: [] };
|
let linkRecords: any = { results: [] };
|
||||||
const requests: Promise<Response>[] = [];
|
const requests: Promise<Response>[] = [];
|
||||||
let nodePromise: Promise<any> | null = null;
|
let nodePromise: Promise<Response> | null = null;
|
||||||
let linkPromise: Promise<any> | null = null;
|
let linkPromise: Promise<Response> | null = null;
|
||||||
// 检查node缓存
|
|
||||||
if (junctionProperties !== "" && junctionProperties !== "elevation") {
|
if (junctionProperties !== "" && junctionProperties !== "elevation") {
|
||||||
const nodeCacheKey = `${query_time}_${junctionProperties}_${schemeName}_${schemeType}`;
|
const nodeCacheKey = buildCacheKey(
|
||||||
|
query_time,
|
||||||
|
junctionProperties,
|
||||||
|
sourceType,
|
||||||
|
"node",
|
||||||
|
schemeName || "",
|
||||||
|
schemeType || ""
|
||||||
|
);
|
||||||
if (nodeCacheRef.current.has(nodeCacheKey)) {
|
if (nodeCacheRef.current.has(nodeCacheKey)) {
|
||||||
nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!;
|
nodeRecords = nodeCacheRef.current.get(nodeCacheKey)!;
|
||||||
} else {
|
} else {
|
||||||
disableDateSelection && schemeName
|
nodePromise =
|
||||||
? (nodePromise = apiFetch(
|
sourceType === "scheme" && schemeName
|
||||||
// `${config.BACKEND_URL}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}&schemename=${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}`,
|
`${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(
|
: 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}`
|
||||||
`${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=node&property=${junctionProperties}`,
|
);
|
||||||
));
|
|
||||||
requests.push(nodePromise);
|
requests.push(nodePromise);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 处理特殊属性名称
|
|
||||||
if (pipeProperties === "unit_headloss") pipeProperties = "headloss";
|
|
||||||
|
|
||||||
// 检查link缓存
|
const normalizedPipeProperties =
|
||||||
if (pipeProperties !== "" && pipeProperties !== "diameter") {
|
pipeProperties === "unit_headloss" ? "headloss" : pipeProperties;
|
||||||
const linkCacheKey = `${query_time}_${pipeProperties}_${schemeName}_${schemeType}`;
|
|
||||||
|
if (normalizedPipeProperties !== "" && normalizedPipeProperties !== "diameter") {
|
||||||
|
const linkCacheKey = buildCacheKey(
|
||||||
|
query_time,
|
||||||
|
normalizedPipeProperties,
|
||||||
|
sourceType,
|
||||||
|
"link",
|
||||||
|
schemeName || "",
|
||||||
|
schemeType || ""
|
||||||
|
);
|
||||||
if (linkCacheRef.current.has(linkCacheKey)) {
|
if (linkCacheRef.current.has(linkCacheKey)) {
|
||||||
linkRecords = linkCacheRef.current.get(linkCacheKey)!;
|
linkRecords = linkCacheRef.current.get(linkCacheKey)!;
|
||||||
} else {
|
} else {
|
||||||
disableDateSelection && schemeName
|
linkPromise =
|
||||||
? (linkPromise = apiFetch(
|
sourceType === "scheme" && schemeName
|
||||||
// `${config.BACKEND_URL}/queryallschemerecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}&schemename=${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=${pipeProperties}`,
|
`${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}`
|
||||||
))
|
)
|
||||||
: (linkPromise = apiFetch(
|
: 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=${normalizedPipeProperties}`
|
||||||
`${config.BACKEND_URL}/api/v1/realtime/query/by-time-property?query_time=${query_time}&type=link&property=${pipeProperties}`,
|
);
|
||||||
));
|
|
||||||
requests.push(linkPromise);
|
requests.push(linkPromise);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待所有有效请求
|
|
||||||
const responses = await Promise.all(requests);
|
const responses = await Promise.all(requests);
|
||||||
|
|
||||||
if (nodePromise) {
|
if (nodePromise) {
|
||||||
const nodeResponse = responses.shift()!;
|
const nodeResponse = responses.shift()!;
|
||||||
if (!nodeResponse.ok)
|
if (!nodeResponse.ok) {
|
||||||
throw new Error(`Node fetch failed: ${nodeResponse.status}`);
|
throw new Error(`Node fetch failed: ${nodeResponse.status}`);
|
||||||
|
}
|
||||||
nodeRecords = await nodeResponse.json();
|
nodeRecords = await nodeResponse.json();
|
||||||
// 缓存数据(修复键以包含 schemeName)
|
|
||||||
nodeCacheRef.current.set(
|
nodeCacheRef.current.set(
|
||||||
`${query_time}_${junctionProperties}_${schemeName}_${schemeType}`,
|
buildCacheKey(
|
||||||
nodeRecords || [],
|
query_time,
|
||||||
|
junctionProperties,
|
||||||
|
sourceType,
|
||||||
|
"node",
|
||||||
|
schemeName || "",
|
||||||
|
schemeType || ""
|
||||||
|
),
|
||||||
|
nodeRecords || []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (linkPromise) {
|
if (linkPromise) {
|
||||||
const linkResponse = responses.shift()!;
|
const linkResponse = responses.shift()!;
|
||||||
if (!linkResponse.ok)
|
if (!linkResponse.ok) {
|
||||||
throw new Error(`Link fetch failed: ${linkResponse.status}`);
|
throw new Error(`Link fetch failed: ${linkResponse.status}`);
|
||||||
|
}
|
||||||
linkRecords = await linkResponse.json();
|
linkRecords = await linkResponse.json();
|
||||||
// 缓存数据(修复键以包含 schemeName)
|
|
||||||
linkCacheRef.current.set(
|
linkCacheRef.current.set(
|
||||||
`${query_time}_${pipeProperties}_${schemeName}_${schemeType}`,
|
buildCacheKey(
|
||||||
linkRecords || [],
|
query_time,
|
||||||
|
normalizedPipeProperties,
|
||||||
|
sourceType,
|
||||||
|
"link",
|
||||||
|
schemeName || "",
|
||||||
|
schemeType || ""
|
||||||
|
),
|
||||||
|
linkRecords || []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 更新状态
|
|
||||||
updateDataStates(nodeRecords.results || [], linkRecords.results || []);
|
updateDataStates(nodeRecords.results || [], linkRecords.results || [], target);
|
||||||
}, [disableDateSelection, updateDataStates]);
|
},
|
||||||
|
[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分钟一个刻度)
|
// 时间刻度数组 (每5分钟一个刻度)
|
||||||
const timeMarks = Array.from({ length: 288 }, (_, i) => ({
|
const timeMarks = Array.from({ length: 288 }, (_, i) => ({
|
||||||
@@ -453,9 +565,9 @@ const Timeline: React.FC<TimelineProps> = ({
|
|||||||
if (!cacheRef.current) return;
|
if (!cacheRef.current) return;
|
||||||
const cacheKeys = Array.from(cacheRef.current.keys());
|
const cacheKeys = Array.from(cacheRef.current.keys());
|
||||||
cacheKeys.forEach((key) => {
|
cacheKeys.forEach((key) => {
|
||||||
const keyParts = key.split("_");
|
const cacheTimeKey = key.split("::")[0];
|
||||||
const cacheDate = keyParts[0].split("T")[0];
|
const cacheDate = cacheTimeKey.split("T")[0];
|
||||||
const cacheTimeStr = keyParts[0].split("T")[1];
|
const cacheTimeStr = cacheTimeKey.split("T")[1];
|
||||||
|
|
||||||
if (cacheDate === dateStr && cacheTimeStr) {
|
if (cacheDate === dateStr && cacheTimeStr) {
|
||||||
const [hours, minutes] = cacheTimeStr.split(":");
|
const [hours, minutes] = cacheTimeStr.split(":");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
|||||||
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||||
import PaletteOutlinedIcon from "@mui/icons-material/PaletteOutlined";
|
import PaletteOutlinedIcon from "@mui/icons-material/PaletteOutlined";
|
||||||
import QueryStatsOutlinedIcon from "@mui/icons-material/QueryStatsOutlined";
|
import QueryStatsOutlinedIcon from "@mui/icons-material/QueryStatsOutlined";
|
||||||
|
import CompareArrowsOutlinedIcon from "@mui/icons-material/CompareArrowsOutlined";
|
||||||
import PropertyPanel from "./PropertyPanel"; // 引入属性面板组件
|
import PropertyPanel from "./PropertyPanel"; // 引入属性面板组件
|
||||||
import DrawPanel from "./DrawPanel"; // 引入绘图面板组件
|
import DrawPanel from "./DrawPanel"; // 引入绘图面板组件
|
||||||
import HistoryDataPanel from "./HistoryDataPanel"; // 引入绘图面板组件
|
import HistoryDataPanel from "./HistoryDataPanel"; // 引入绘图面板组件
|
||||||
@@ -34,12 +35,14 @@ interface ToolbarProps {
|
|||||||
queryType?: string; // 可选的查询类型参数
|
queryType?: string; // 可选的查询类型参数
|
||||||
schemeType?: string; // 可选的方案类型参数
|
schemeType?: string; // 可选的方案类型参数
|
||||||
HistoryPanel?: React.FC<any>; // 可选的自定义历史数据面板
|
HistoryPanel?: React.FC<any>; // 可选的自定义历史数据面板
|
||||||
|
enableCompare?: boolean;
|
||||||
}
|
}
|
||||||
const Toolbar: React.FC<ToolbarProps> = ({
|
const Toolbar: React.FC<ToolbarProps> = ({
|
||||||
hiddenButtons,
|
hiddenButtons,
|
||||||
queryType,
|
queryType,
|
||||||
schemeType,
|
schemeType,
|
||||||
HistoryPanel,
|
HistoryPanel,
|
||||||
|
enableCompare = false,
|
||||||
}) => {
|
}) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const data = useData();
|
const data = useData();
|
||||||
@@ -55,6 +58,17 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
const currentTime = data?.currentTime;
|
const currentTime = data?.currentTime;
|
||||||
const selectedDate = data?.selectedDate;
|
const selectedDate = data?.selectedDate;
|
||||||
const schemeName = data?.schemeName;
|
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)
|
// Chat tool action → direct featureInfos override (bypasses OL Feature lookup)
|
||||||
const [chatPanelFeatureInfos, setChatPanelFeatureInfos] = useState<
|
const [chatPanelFeatureInfos, setChatPanelFeatureInfos] = useState<
|
||||||
@@ -853,6 +867,15 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
onClick={() => handleToolClick("style")}
|
onClick={() => handleToolClick("style")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{enableCompare && (
|
||||||
|
<ToolbarButton
|
||||||
|
icon={<CompareArrowsOutlinedIcon />}
|
||||||
|
name={isCompareMode ? "关闭对比" : "双屏对比"}
|
||||||
|
isActive={isCompareMode}
|
||||||
|
onClick={() => toggleCompareMode?.()}
|
||||||
|
disabled={!canToggleCompare}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showPropertyPanel && <PropertyPanel {...getFeatureProperties()} />}
|
{showPropertyPanel && <PropertyPanel {...getFeatureProperties()} />}
|
||||||
{showDrawPanel && map && <DrawPanel />}
|
{showDrawPanel && map && <DrawPanel />}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useCallback,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Map as OlMap, VectorTile } from "ol";
|
import { Map as OlMap, VectorTile } from "ol";
|
||||||
@@ -49,6 +50,13 @@ interface DataContextType {
|
|||||||
setCurrentJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>;
|
setCurrentJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||||
currentPipeCalData?: any[]; // 当前计算结果
|
currentPipeCalData?: any[]; // 当前计算结果
|
||||||
setCurrentPipeCalData?: React.Dispatch<React.SetStateAction<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; // 是否显示节点文本
|
showJunctionText?: boolean; // 是否显示节点文本
|
||||||
showPipeText?: boolean; // 是否显示管道文本
|
showPipeText?: boolean; // 是否显示管道文本
|
||||||
showJunctionId?: boolean; // 是否显示节点ID
|
showJunctionId?: boolean; // 是否显示节点ID
|
||||||
@@ -69,6 +77,10 @@ interface DataContextType {
|
|||||||
setPipeText?: React.Dispatch<React.SetStateAction<string>>;
|
setPipeText?: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setContours?: React.Dispatch<React.SetStateAction<any[]>>;
|
setContours?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||||
deckLayer?: DeckLayer;
|
deckLayer?: DeckLayer;
|
||||||
|
compareDeckLayer?: DeckLayer;
|
||||||
|
deckLayers?: DeckLayer[];
|
||||||
|
compareMap?: OlMap;
|
||||||
|
maps?: OlMap[];
|
||||||
diameterRange?: [number, number];
|
diameterRange?: [number, number];
|
||||||
elevationRange?: [number, number];
|
elevationRange?: [number, number];
|
||||||
}
|
}
|
||||||
@@ -128,12 +140,18 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
|
|
||||||
const mapRef = useRef<HTMLDivElement | null>(null);
|
const mapRef = useRef<HTMLDivElement | null>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement | 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 deckLayerRef = useRef<DeckLayer | null>(null);
|
||||||
|
const compareDeckLayerRef = useRef<DeckLayer | null>(null);
|
||||||
const isDisposingRef = useRef(false);
|
const isDisposingRef = useRef(false);
|
||||||
|
const isCompareDisposingRef = useRef(false);
|
||||||
const pendingTimeoutsRef = useRef<number[]>([]);
|
const pendingTimeoutsRef = useRef<number[]>([]);
|
||||||
|
|
||||||
const [map, setMap] = useState<OlMap>();
|
const [map, setMap] = useState<OlMap>();
|
||||||
const [deckLayer, setDeckLayer] = useState<DeckLayer>();
|
const [deckLayer, setDeckLayer] = useState<DeckLayer>();
|
||||||
|
const [compareMap, setCompareMap] = useState<OlMap>();
|
||||||
|
const [compareDeckLayer, setCompareDeckLayer] = useState<DeckLayer>();
|
||||||
// currentCalData 用于存储当前计算结果
|
// currentCalData 用于存储当前计算结果
|
||||||
const [currentTime, setCurrentTime] = useState<number>(-1); // 默认选择当前时间
|
const [currentTime, setCurrentTime] = useState<number>(-1); // 默认选择当前时间
|
||||||
// const [selectedDate, setSelectedDate] = useState<Date>(new Date("2025-9-17"));
|
// 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 [currentPipeCalData, setCurrentPipeCalData] = useState<any[]>([]);
|
||||||
|
const [compareJunctionCalData, setCompareJunctionCalData] = useState<any[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [comparePipeCalData, setComparePipeCalData] = useState<any[]>([]);
|
||||||
|
const [isCompareMode, setCompareMode] = useState(false);
|
||||||
// junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染
|
// junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染
|
||||||
// currentJunctionCalData 和 currentPipeCalData 变化时会新增并更新 junctionData 和 pipeData 的计算属性值
|
// currentJunctionCalData 和 currentPipeCalData 变化时会新增并更新 junctionData 和 pipeData 的计算属性值
|
||||||
const [junctionData, setJunctionDataState] = useState<any[]>([]);
|
const [junctionData, setJunctionDataState] = useState<any[]>([]);
|
||||||
@@ -201,6 +224,37 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
});
|
});
|
||||||
}, [pipeData, currentPipeCalData, pipeText]);
|
}, [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<
|
const [diameterRange, setDiameterRange] = useState<
|
||||||
[number, number] | undefined
|
[number, number] | undefined
|
||||||
>();
|
>();
|
||||||
@@ -208,6 +262,24 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
[number, number] | undefined
|
[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 setJunctionData = (newData: any[]) => {
|
||||||
const uniqueNewData = newData.filter((item) => {
|
const uniqueNewData = newData.filter((item) => {
|
||||||
if (!item || !item.id) return false;
|
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.
|
// The map and layer instances are intentionally rebuilt only when workspace or extent changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapRef.current) return;
|
if (!mapRef.current) return;
|
||||||
@@ -857,19 +1101,142 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [MAP_WORKSPACE, MAP_EXTENT]);
|
}, [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 图层
|
// 当数据变化时,更新 deck.gl 图层
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDisposingRef.current) return;
|
const syncDeckOverlay = (
|
||||||
const deckLayer = deckLayerRef.current;
|
targetDeckLayer: DeckLayer | null,
|
||||||
if (!deckLayer) return; // 如果 deck 实例还未创建,则退出
|
targetJunctionData: any[],
|
||||||
if (deckLayer.isDisposedLayer()) return;
|
targetPipeData: any[],
|
||||||
if (!mergedJunctionData.length) return;
|
disposing: boolean,
|
||||||
if (!mergedPipeData.length) return;
|
) => {
|
||||||
const junctionTextLayer = new TextLayer({
|
if (disposing || !targetDeckLayer || targetDeckLayer.isDisposedLayer()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const shouldShowJunctionText =
|
||||||
|
(showJunctionTextLayer || showJunctionId) &&
|
||||||
|
currentZoom >= 15 &&
|
||||||
|
currentZoom <= 24 &&
|
||||||
|
targetJunctionData.length > 0;
|
||||||
|
const shouldShowPipeText =
|
||||||
|
(showPipeTextLayer || showPipeId) &&
|
||||||
|
currentZoom >= 15 &&
|
||||||
|
currentZoom <= 24 &&
|
||||||
|
targetPipeData.length > 0;
|
||||||
|
const shouldShowContour =
|
||||||
|
showContourLayer &&
|
||||||
|
currentZoom >= 11 &&
|
||||||
|
currentZoom <= 24 &&
|
||||||
|
targetJunctionData.length > 0;
|
||||||
|
|
||||||
|
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",
|
id: "junctionTextLayer",
|
||||||
name: "节点文字",
|
name: "节点文字",
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
data: mergedJunctionData,
|
data: targetJunctionData,
|
||||||
getPosition: (d: any) => d.position,
|
getPosition: (d: any) => d.position,
|
||||||
fontFamily: "Monaco, monospace",
|
fontFamily: "Monaco, monospace",
|
||||||
getText: (d: any) => {
|
getText: (d: any) => {
|
||||||
@@ -884,15 +1251,12 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
},
|
},
|
||||||
getSize: 14,
|
getSize: 14,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
getColor: [33, 37, 41], // 深灰色,在灰白背景上清晰可见
|
getColor: [33, 37, 41],
|
||||||
getAngle: 0,
|
getAngle: 0,
|
||||||
getTextAnchor: "middle",
|
getTextAnchor: "middle",
|
||||||
getAlignmentBaseline: "center",
|
getAlignmentBaseline: "center",
|
||||||
getPixelOffset: [0, -10],
|
getPixelOffset: [0, -10],
|
||||||
visible:
|
visible: true,
|
||||||
(showJunctionTextLayer || showJunctionId) &&
|
|
||||||
currentZoom >= 15 &&
|
|
||||||
currentZoom <= 24,
|
|
||||||
updateTriggers: {
|
updateTriggers: {
|
||||||
getText: [showJunctionId, showJunctionTextLayer, junctionText],
|
getText: [showJunctionId, showJunctionTextLayer, junctionText],
|
||||||
},
|
},
|
||||||
@@ -906,15 +1270,15 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
fontSize: 64,
|
fontSize: 64,
|
||||||
buffer: 6,
|
buffer: 6,
|
||||||
},
|
},
|
||||||
// outlineWidth: 3,
|
})
|
||||||
// outlineColor: [255, 255, 255, 220],
|
: null;
|
||||||
});
|
|
||||||
|
|
||||||
const pipeTextLayer = new TextLayer({
|
const pipeTextLayer = shouldShowPipeText
|
||||||
|
? new TextLayer({
|
||||||
id: "pipeTextLayer",
|
id: "pipeTextLayer",
|
||||||
name: "管道文字",
|
name: "管道文字",
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
data: mergedPipeData,
|
data: targetPipeData,
|
||||||
getPosition: (d: any) => d.position,
|
getPosition: (d: any) => d.position,
|
||||||
fontFamily: "Monaco, monospace",
|
fontFamily: "Monaco, monospace",
|
||||||
getText: (d: any) => {
|
getText: (d: any) => {
|
||||||
@@ -936,15 +1300,12 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
},
|
},
|
||||||
getSize: 14,
|
getSize: 14,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
getColor: [33, 37, 41], // 深灰色
|
getColor: [33, 37, 41],
|
||||||
getAngle: (d: any) => d.angle || 0,
|
getAngle: (d: any) => d.angle || 0,
|
||||||
getPixelOffset: [0, -8],
|
getPixelOffset: [0, -8],
|
||||||
getTextAnchor: "middle",
|
getTextAnchor: "middle",
|
||||||
getAlignmentBaseline: "bottom",
|
getAlignmentBaseline: "bottom",
|
||||||
visible:
|
visible: true,
|
||||||
(showPipeTextLayer || showPipeId) &&
|
|
||||||
currentZoom >= 15 &&
|
|
||||||
currentZoom <= 24,
|
|
||||||
updateTriggers: {
|
updateTriggers: {
|
||||||
getText: [showPipeId, showPipeTextLayer, pipeText],
|
getText: [showPipeId, showPipeTextLayer, pipeText],
|
||||||
},
|
},
|
||||||
@@ -958,14 +1319,14 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
fontSize: 64,
|
fontSize: 64,
|
||||||
buffer: 6,
|
buffer: 6,
|
||||||
},
|
},
|
||||||
// outlineWidth: 3,
|
})
|
||||||
// outlineColor: [255, 255, 255, 220],
|
: null;
|
||||||
});
|
|
||||||
|
|
||||||
const contourLayer = new ContourLayer({
|
const contourLayer = shouldShowContour
|
||||||
|
? new ContourLayer({
|
||||||
id: "junctionContourLayer",
|
id: "junctionContourLayer",
|
||||||
name: "等值线",
|
name: "等值线",
|
||||||
data: mergedJunctionData,
|
data: targetJunctionData,
|
||||||
aggregation: "MEAN",
|
aggregation: "MEAN",
|
||||||
cellSize: 600,
|
cellSize: 600,
|
||||||
strokeWidth: 0,
|
strokeWidth: 0,
|
||||||
@@ -974,31 +1335,50 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
getWeight: (d: any) =>
|
getWeight: (d: any) =>
|
||||||
(d[junctionText] as number) < 0 ? 0 : (d[junctionText] as number),
|
(d[junctionText] as number) < 0 ? 0 : (d[junctionText] as number),
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
visible: showContourLayer && currentZoom >= 11 && currentZoom <= 24,
|
visible: true,
|
||||||
updateTriggers: {
|
updateTriggers: {
|
||||||
// 当 mergedJunctionData 内部数据更新时,通知 getWeight 重新计算
|
getWeight: [targetJunctionData, junctionText],
|
||||||
getWeight: [mergedJunctionData, junctionText],
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
if (deckLayer.getDeckLayerById("junctionTextLayer")) {
|
: null;
|
||||||
// 传入完整 layer 实例以保证 clone/替换时保留 layer 类型和方法
|
|
||||||
deckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer);
|
if (junctionTextLayer && targetDeckLayer.getDeckLayerById("junctionTextLayer")) {
|
||||||
} else {
|
targetDeckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer);
|
||||||
deckLayer.addDeckLayer(junctionTextLayer);
|
} else if (junctionTextLayer) {
|
||||||
|
targetDeckLayer.addDeckLayer(junctionTextLayer);
|
||||||
}
|
}
|
||||||
if (deckLayer.getDeckLayerById("pipeTextLayer")) {
|
if (pipeTextLayer && targetDeckLayer.getDeckLayerById("pipeTextLayer")) {
|
||||||
deckLayer.updateDeckLayer("pipeTextLayer", pipeTextLayer);
|
targetDeckLayer.updateDeckLayer("pipeTextLayer", pipeTextLayer);
|
||||||
} else {
|
} else if (pipeTextLayer) {
|
||||||
deckLayer.addDeckLayer(pipeTextLayer);
|
targetDeckLayer.addDeckLayer(pipeTextLayer);
|
||||||
}
|
}
|
||||||
if (deckLayer.getDeckLayerById("junctionContourLayer")) {
|
if (contourLayer && targetDeckLayer.getDeckLayerById("junctionContourLayer")) {
|
||||||
deckLayer.updateDeckLayer("junctionContourLayer", contourLayer);
|
targetDeckLayer.updateDeckLayer("junctionContourLayer", contourLayer);
|
||||||
} else {
|
} else if (contourLayer) {
|
||||||
deckLayer.addDeckLayer(contourLayer);
|
targetDeckLayer.addDeckLayer(contourLayer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
syncDeckOverlay(
|
||||||
|
deckLayerRef.current,
|
||||||
|
mergedJunctionData,
|
||||||
|
mergedPipeData,
|
||||||
|
isDisposingRef.current,
|
||||||
|
);
|
||||||
|
if (isCompareMode) {
|
||||||
|
syncDeckOverlay(
|
||||||
|
compareDeckLayerRef.current,
|
||||||
|
mergedCompareJunctionData,
|
||||||
|
mergedComparePipeData,
|
||||||
|
isCompareDisposingRef.current,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
mergedJunctionData,
|
mergedJunctionData,
|
||||||
mergedPipeData,
|
mergedPipeData,
|
||||||
|
mergedCompareJunctionData,
|
||||||
|
mergedComparePipeData,
|
||||||
|
isCompareMode,
|
||||||
junctionText,
|
junctionText,
|
||||||
pipeText,
|
pipeText,
|
||||||
currentZoom,
|
currentZoom,
|
||||||
@@ -1012,57 +1392,69 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
|
|
||||||
// 控制流动动画开关
|
// 控制流动动画开关
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDisposingRef.current) return;
|
flowAnimation.current = pipeText === "flow" && currentPipeCalData.length > 0;
|
||||||
if (pipeText === "flow" && currentPipeCalData.length > 0) {
|
const shouldShowWaterflow =
|
||||||
flowAnimation.current = true;
|
isWaterflowLayerAvailable &&
|
||||||
} else {
|
showWaterflowLayer &&
|
||||||
flowAnimation.current = false;
|
flowAnimation.current &&
|
||||||
|
currentZoom >= 12 &&
|
||||||
|
currentZoom <= 24;
|
||||||
|
|
||||||
|
let animationFrameId: number;
|
||||||
|
|
||||||
|
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 deckLayer = deckLayerRef.current;
|
|
||||||
if (!deckLayer) return; // 如果 deck 实例还未创建,则退出
|
|
||||||
|
|
||||||
let animationFrameId: number; // 保存 requestAnimationFrame 的 ID
|
|
||||||
|
|
||||||
// 动画循环
|
|
||||||
const animate = () => {
|
|
||||||
if (isDisposingRef.current || deckLayer.isDisposedLayer()) return;
|
|
||||||
// 动画总时长(秒)
|
|
||||||
const animationDuration = 10;
|
const animationDuration = 10;
|
||||||
const bufferTime = 2;
|
const bufferTime = 2;
|
||||||
const loopLength = animationDuration + bufferTime;
|
const loopLength = animationDuration + bufferTime;
|
||||||
const currentTime = (Date.now() / 1000) % loopLength;
|
const currentFrameTime = (Date.now() / 1000) % loopLength;
|
||||||
|
|
||||||
const waterflowLayer = new TripsLayer({
|
const waterflowLayer = new TripsLayer({
|
||||||
id: "waterflowLayer",
|
id: "waterflowLayer",
|
||||||
name: "水流",
|
name: "水流",
|
||||||
data: mergedPipeData,
|
data: targetPipeData,
|
||||||
getPath: (d) => d.path,
|
getPath: (d) => d.path,
|
||||||
getTimestamps: (d) => {
|
getTimestamps: (d) => d.timestamps,
|
||||||
return d.timestamps; // 这些应该是与 currentTime 匹配的数值
|
|
||||||
},
|
|
||||||
getColor: [0, 220, 255],
|
getColor: [0, 220, 255],
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
visible:
|
visible: true,
|
||||||
isWaterflowLayerAvailable &&
|
|
||||||
showWaterflowLayer &&
|
|
||||||
flowAnimation.current && // 保持动画标志作为可见性的一部分
|
|
||||||
currentZoom >= 12 &&
|
|
||||||
currentZoom <= 24,
|
|
||||||
widthMinPixels: 5,
|
widthMinPixels: 5,
|
||||||
jointRounded: true, // 拐角变圆
|
jointRounded: true,
|
||||||
// capRounded: true, // 端点变圆
|
trailLength: 2,
|
||||||
trailLength: 2, // 水流尾迹淡出时间
|
currentTime: currentFrameTime,
|
||||||
currentTime: currentTime,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (deckLayer.getDeckLayerById("waterflowLayer")) {
|
if (targetDeckLayer.getDeckLayerById("waterflowLayer")) {
|
||||||
deckLayer.updateDeckLayer("waterflowLayer", waterflowLayer);
|
targetDeckLayer.updateDeckLayer("waterflowLayer", waterflowLayer);
|
||||||
} else {
|
} else {
|
||||||
deckLayer.addDeckLayer(waterflowLayer);
|
targetDeckLayer.addDeckLayer(waterflowLayer);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 只有在需要动画时才请求下一帧,但图层已经添加到了 deckLayer 中
|
const animate = () => {
|
||||||
if (flowAnimation.current) {
|
syncWaterflowLayer(
|
||||||
|
deckLayerRef.current,
|
||||||
|
mergedPipeData,
|
||||||
|
isDisposingRef.current,
|
||||||
|
);
|
||||||
|
if (isCompareMode) {
|
||||||
|
syncWaterflowLayer(
|
||||||
|
compareDeckLayerRef.current,
|
||||||
|
mergedComparePipeData,
|
||||||
|
isCompareDisposingRef.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (shouldShowWaterflow) {
|
||||||
animationFrameId = requestAnimationFrame(animate);
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1078,6 +1470,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
currentPipeCalData,
|
currentPipeCalData,
|
||||||
currentZoom,
|
currentZoom,
|
||||||
mergedPipeData,
|
mergedPipeData,
|
||||||
|
mergedComparePipeData,
|
||||||
|
isCompareMode,
|
||||||
pipeText,
|
pipeText,
|
||||||
isWaterflowLayerAvailable,
|
isWaterflowLayerAvailable,
|
||||||
showWaterflowLayer,
|
showWaterflowLayer,
|
||||||
@@ -1097,6 +1491,13 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
setCurrentJunctionCalData,
|
setCurrentJunctionCalData,
|
||||||
currentPipeCalData,
|
currentPipeCalData,
|
||||||
setCurrentPipeCalData,
|
setCurrentPipeCalData,
|
||||||
|
compareJunctionCalData,
|
||||||
|
setCompareJunctionCalData,
|
||||||
|
comparePipeCalData,
|
||||||
|
setComparePipeCalData,
|
||||||
|
isCompareMode,
|
||||||
|
setCompareMode,
|
||||||
|
toggleCompareMode,
|
||||||
setShowJunctionTextLayer,
|
setShowJunctionTextLayer,
|
||||||
setShowPipeTextLayer,
|
setShowPipeTextLayer,
|
||||||
setShowJunctionId,
|
setShowJunctionId,
|
||||||
@@ -1115,17 +1516,50 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
pipeText,
|
pipeText,
|
||||||
setContours,
|
setContours,
|
||||||
deckLayer,
|
deckLayer,
|
||||||
|
compareDeckLayer,
|
||||||
|
deckLayers,
|
||||||
|
compareMap,
|
||||||
|
maps,
|
||||||
diameterRange,
|
diameterRange,
|
||||||
elevationRange,
|
elevationRange,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MapContext.Provider value={map}>
|
<MapContext.Provider value={map}>
|
||||||
<div className="relative w-full h-full">
|
<div className="relative w-full h-full">
|
||||||
|
<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>
|
<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 />
|
<MapTools />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<canvas ref={canvasRef} />
|
|
||||||
</MapContext.Provider>
|
</MapContext.Provider>
|
||||||
</DataContext.Provider>
|
</DataContext.Provider>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user