diff --git a/src/app/(main)/health-risk-analysis/loading.tsx b/src/app/(main)/(map)/health-risk-analysis/loading.tsx similarity index 100% rename from src/app/(main)/health-risk-analysis/loading.tsx rename to src/app/(main)/(map)/health-risk-analysis/loading.tsx diff --git a/src/app/(main)/(map)/health-risk-analysis/page.tsx b/src/app/(main)/(map)/health-risk-analysis/page.tsx new file mode 100644 index 0000000..779e035 --- /dev/null +++ b/src/app/(main)/(map)/health-risk-analysis/page.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Timeline from "@components/olmap/HealthRiskAnalysis/Timeline"; +import MapToolbar from "@components/olmap/core/Controls/Toolbar"; +import { HealthRiskProvider } from "@components/olmap/HealthRiskAnalysis/HealthRiskContext"; +import HealthRiskStatistics from "@components/olmap/HealthRiskAnalysis/HealthRiskStatistics"; +import PredictDataPanel from "@components/olmap/HealthRiskAnalysis/PredictDataPanel"; +import StyleLegend from "@components/olmap/core/Controls/StyleLegend"; +import { + RAINBOW_COLORS, + RISK_BREAKS, +} from "@components/olmap/HealthRiskAnalysis/types"; +import { Box } from "@mui/material"; + +export default function Home() { + return ( + + + + + + + + + ); +} diff --git a/src/app/(main)/hydraulic-simulation/burst-detection/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-detection/loading.tsx similarity index 100% rename from src/app/(main)/hydraulic-simulation/burst-detection/loading.tsx rename to src/app/(main)/(map)/hydraulic-simulation/burst-detection/loading.tsx diff --git a/src/app/(main)/(map)/hydraulic-simulation/burst-detection/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-detection/page.tsx new file mode 100644 index 0000000..ca5408c --- /dev/null +++ b/src/app/(main)/(map)/hydraulic-simulation/burst-detection/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import MapToolbar from "@components/olmap/core/Controls/Toolbar"; +import BurstDetectionPanel from "@/components/olmap/BurstDetection/BurstDetectionPanel"; + +export default function Home() { + return ( + <> + + + + ); +} diff --git a/src/app/(main)/hydraulic-simulation/burst-location/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-location/loading.tsx similarity index 100% rename from src/app/(main)/hydraulic-simulation/burst-location/loading.tsx rename to src/app/(main)/(map)/hydraulic-simulation/burst-location/loading.tsx diff --git a/src/app/(main)/(map)/hydraulic-simulation/burst-location/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-location/page.tsx new file mode 100644 index 0000000..d5770b1 --- /dev/null +++ b/src/app/(main)/(map)/hydraulic-simulation/burst-location/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import MapToolbar from "@components/olmap/core/Controls/Toolbar"; +import BurstLocationPanel from "@/components/olmap/BurstLocation/BurstLocationPanel"; + +export default function Home() { + return ( + <> + + + + ); +} diff --git a/src/app/(main)/hydraulic-simulation/burst-simulation/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/loading.tsx similarity index 100% rename from src/app/(main)/hydraulic-simulation/burst-simulation/loading.tsx rename to src/app/(main)/(map)/hydraulic-simulation/burst-simulation/loading.tsx diff --git a/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/page.tsx new file mode 100644 index 0000000..699997f --- /dev/null +++ b/src/app/(main)/(map)/hydraulic-simulation/burst-simulation/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import MapToolbar from "@components/olmap/core/Controls/Toolbar"; +import BurstPipeAnalysisPanel from "@/components/olmap/BurstSimulation/BurstPipeAnalysisPanel"; + +export default function Home() { + return ( + <> + + + + ); +} diff --git a/src/app/(main)/hydraulic-simulation/contaminant-simulation/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/loading.tsx similarity index 100% rename from src/app/(main)/hydraulic-simulation/contaminant-simulation/loading.tsx rename to src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/loading.tsx diff --git a/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/page.tsx new file mode 100644 index 0000000..6addcb6 --- /dev/null +++ b/src/app/(main)/(map)/hydraulic-simulation/contaminant-simulation/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import MapToolbar from "@components/olmap/core/Controls/Toolbar"; +import WaterQualityPanel from "@/components/olmap/ContaminantSimulation/WaterQualityPanel"; + +export default function Home() { + return ( + <> + + + + ); +} diff --git a/src/app/(main)/hydraulic-simulation/dma-leak-detection/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/loading.tsx similarity index 100% rename from src/app/(main)/hydraulic-simulation/dma-leak-detection/loading.tsx rename to src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/loading.tsx diff --git a/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/page.tsx new file mode 100644 index 0000000..39058bb --- /dev/null +++ b/src/app/(main)/(map)/hydraulic-simulation/dma-leak-detection/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +import MapToolbar from "@components/olmap/core/Controls/Toolbar"; +import DMALeakDetectionPanel from "@/components/olmap/DMALeakDetection/DMALeakDetectionPanel"; + +export default function Home() { + return ( + <> + + + + ); +} diff --git a/src/app/(main)/hydraulic-simulation/flushing-analysis/loading.tsx b/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/loading.tsx similarity index 100% rename from src/app/(main)/hydraulic-simulation/flushing-analysis/loading.tsx rename to src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/loading.tsx diff --git a/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/page.tsx b/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/page.tsx new file mode 100644 index 0000000..19df843 --- /dev/null +++ b/src/app/(main)/(map)/hydraulic-simulation/flushing-analysis/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import MapToolbar from "@components/olmap/core/Controls/Toolbar"; +import FlushingAnalysisPanel from "@/components/olmap/FlushingAnalysis/FlushingAnalysisPanel"; + +export default function Home() { + return ( + <> + + + + ); +} diff --git a/src/app/(main)/(map)/layout.tsx b/src/app/(main)/(map)/layout.tsx new file mode 100644 index 0000000..18c0bae --- /dev/null +++ b/src/app/(main)/(map)/layout.tsx @@ -0,0 +1,9 @@ +"use client"; + +import type { ReactNode } from "react"; + +import MapComponent from "@components/olmap/core/MapComponent"; + +export default function MapLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/src/app/(main)/monitoring-place-optimization/loading.tsx b/src/app/(main)/(map)/monitoring-place-optimization/loading.tsx similarity index 100% rename from src/app/(main)/monitoring-place-optimization/loading.tsx rename to src/app/(main)/(map)/monitoring-place-optimization/loading.tsx diff --git a/src/app/(main)/(map)/monitoring-place-optimization/page.tsx b/src/app/(main)/(map)/monitoring-place-optimization/page.tsx new file mode 100644 index 0000000..0272841 --- /dev/null +++ b/src/app/(main)/(map)/monitoring-place-optimization/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import MapToolbar from "@components/olmap/core/Controls/Toolbar"; +import MonitoringPlaceOptimizationPanel from "@components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel"; + +export default function Home() { + return ( + <> + + + + ); +} diff --git a/src/app/(main)/network-simulation/loading.tsx b/src/app/(main)/(map)/network-simulation/loading.tsx similarity index 100% rename from src/app/(main)/network-simulation/loading.tsx rename to src/app/(main)/(map)/network-simulation/loading.tsx diff --git a/src/app/(main)/network-simulation/page.tsx b/src/app/(main)/(map)/network-simulation/page.tsx similarity index 60% rename from src/app/(main)/network-simulation/page.tsx rename to src/app/(main)/(map)/network-simulation/page.tsx index 18ec7c7..705afe1 100644 --- a/src/app/(main)/network-simulation/page.tsx +++ b/src/app/(main)/(map)/network-simulation/page.tsx @@ -1,7 +1,6 @@ "use client"; import { useCallback, useState } from "react"; -import MapComponent from "@components/olmap/core/MapComponent"; import Timeline from "@components/olmap/core/Controls/Timeline"; import MapToolbar from "@components/olmap/core/Controls/Toolbar"; @@ -22,17 +21,15 @@ export default function Home() { }, []); return ( -
- - - - - - -
+ <> + + + + + ); } diff --git a/src/app/(main)/scada-data-cleaning/loading.tsx b/src/app/(main)/(map)/scada-data-cleaning/loading.tsx similarity index 100% rename from src/app/(main)/scada-data-cleaning/loading.tsx rename to src/app/(main)/(map)/scada-data-cleaning/loading.tsx diff --git a/src/app/(main)/scada-data-cleaning/page.tsx b/src/app/(main)/(map)/scada-data-cleaning/page.tsx similarity index 54% rename from src/app/(main)/scada-data-cleaning/page.tsx rename to src/app/(main)/(map)/scada-data-cleaning/page.tsx index 8458924..592a3b5 100644 --- a/src/app/(main)/scada-data-cleaning/page.tsx +++ b/src/app/(main)/(map)/scada-data-cleaning/page.tsx @@ -1,7 +1,6 @@ "use client"; import { useCallback, useState } from "react"; -import MapComponent from "@components/olmap/core/MapComponent"; import MapToolbar from "@components/olmap/core/Controls/Toolbar"; import SCADADeviceList from "@components/olmap/SCADA/SCADADeviceList"; @@ -21,21 +20,19 @@ export default function Home() { }, []); return ( -
- - - - - -
+ <> + + + + ); } diff --git a/src/app/(main)/health-risk-analysis/page.tsx b/src/app/(main)/health-risk-analysis/page.tsx deleted file mode 100644 index f6c014d..0000000 --- a/src/app/(main)/health-risk-analysis/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import MapComponent from "@components/olmap/core/MapComponent"; -import Timeline from "@components/olmap/HealthRiskAnalysis/Timeline"; -import MapToolbar from "@components/olmap/core/Controls/Toolbar"; -import { HealthRiskProvider } from "@components/olmap/HealthRiskAnalysis/HealthRiskContext"; -import HealthRiskStatistics from "@components/olmap/HealthRiskAnalysis/HealthRiskStatistics"; -import PredictDataPanel from "@components/olmap/HealthRiskAnalysis/PredictDataPanel"; -import StyleLegend from "@components/olmap/core/Controls/StyleLegend"; -import { - RAINBOW_COLORS, - RISK_BREAKS, -} from "@components/olmap/HealthRiskAnalysis/types"; -import { Box } from "@mui/material"; - -export default function Home() { - return ( -
- - - - - - - - - - -
- ); -} diff --git a/src/app/(main)/hydraulic-simulation/burst-detection/page.tsx b/src/app/(main)/hydraulic-simulation/burst-detection/page.tsx deleted file mode 100644 index df2ddb9..0000000 --- a/src/app/(main)/hydraulic-simulation/burst-detection/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import MapComponent from "@components/olmap/core/MapComponent"; -import MapToolbar from "@components/olmap/core/Controls/Toolbar"; -import BurstDetectionPanel from "@/components/olmap/BurstDetection/BurstDetectionPanel"; - -export default function Home() { - return ( -
- - - - -
- ); -} diff --git a/src/app/(main)/hydraulic-simulation/burst-location/page.tsx b/src/app/(main)/hydraulic-simulation/burst-location/page.tsx deleted file mode 100644 index 977ef8e..0000000 --- a/src/app/(main)/hydraulic-simulation/burst-location/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import MapComponent from "@components/olmap/core/MapComponent"; -import MapToolbar from "@components/olmap/core/Controls/Toolbar"; -import BurstLocationPanel from "@/components/olmap/BurstLocation/BurstLocationPanel"; - -export default function Home() { - return ( -
- - - - -
- ); -} diff --git a/src/app/(main)/hydraulic-simulation/burst-simulation/page.tsx b/src/app/(main)/hydraulic-simulation/burst-simulation/page.tsx deleted file mode 100644 index 86f7864..0000000 --- a/src/app/(main)/hydraulic-simulation/burst-simulation/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import MapComponent from "@components/olmap/core/MapComponent"; -import MapToolbar from "@components/olmap/core/Controls/Toolbar"; -import BurstPipeAnalysisPanel from "@/components/olmap/BurstSimulation/BurstPipeAnalysisPanel"; - -export default function Home() { - return ( -
- - - - -
- ); -} diff --git a/src/app/(main)/hydraulic-simulation/contaminant-simulation/page.tsx b/src/app/(main)/hydraulic-simulation/contaminant-simulation/page.tsx deleted file mode 100644 index 265c631..0000000 --- a/src/app/(main)/hydraulic-simulation/contaminant-simulation/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import MapComponent from "@components/olmap/core/MapComponent"; -import MapToolbar from "@components/olmap/core/Controls/Toolbar"; -import WaterQualityPanel from "@/components/olmap/ContaminantSimulation/WaterQualityPanel"; - -export default function Home() { - return ( -
- - - - -
- ); -} diff --git a/src/app/(main)/hydraulic-simulation/dma-leak-detection/page.tsx b/src/app/(main)/hydraulic-simulation/dma-leak-detection/page.tsx deleted file mode 100644 index 68fc86a..0000000 --- a/src/app/(main)/hydraulic-simulation/dma-leak-detection/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import MapComponent from "@components/olmap/core/MapComponent"; -import MapToolbar from "@components/olmap/core/Controls/Toolbar"; -import DMALeakDetectionPanel from "@/components/olmap/DMALeakDetection/DMALeakDetectionPanel"; - -export default function Home() { - return ( -
- - - - -
- ); -} diff --git a/src/app/(main)/hydraulic-simulation/flushing-analysis/page.tsx b/src/app/(main)/hydraulic-simulation/flushing-analysis/page.tsx deleted file mode 100644 index 05e45cb..0000000 --- a/src/app/(main)/hydraulic-simulation/flushing-analysis/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import MapComponent from "@components/olmap/core/MapComponent"; -import MapToolbar from "@components/olmap/core/Controls/Toolbar"; -import FlushingAnalysisPanel from "@/components/olmap/FlushingAnalysis/FlushingAnalysisPanel"; - -export default function Home() { - return ( -
- - - - -
- ); -} diff --git a/src/app/(main)/monitoring-place-optimization/page.tsx b/src/app/(main)/monitoring-place-optimization/page.tsx deleted file mode 100644 index 8ce7202..0000000 --- a/src/app/(main)/monitoring-place-optimization/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -"use client"; - -import MapComponent from "@components/olmap/core/MapComponent"; -import MapToolbar from "@components/olmap/core/Controls/Toolbar"; -import MonitoringPlaceOptimizationPanel from "@components/olmap/MonitoringPlaceOptimization/MonitoringPlaceOptimizationPanel"; -export default function Home() { - return ( -
- - - - -
- ); -} diff --git a/src/components/olmap/core/Controls/BaseLayers.tsx b/src/components/olmap/core/Controls/BaseLayers.tsx index aa43007..8685f67 100644 --- a/src/components/olmap/core/Controls/BaseLayers.tsx +++ b/src/components/olmap/core/Controls/BaseLayers.tsx @@ -14,8 +14,21 @@ import mapboxStreets from "@assets/map/layers/mapbox-streets.png"; import clsx from "clsx"; import { MAPBOX_TOKEN, TIANDITU_TOKEN } from "@config/config"; import type { Map as OlMap } from "ol"; +import { markMapResourcePersistent } from "../mapLifecycle"; const INITIAL_LAYER = "mapbox-light"; +const BASE_LAYER_METADATA = [ + { id: "mapbox-light", name: "默认地图", img: mapboxLight.src }, + { id: "mapbox-satellite", name: "卫星地图", img: mapboxSatellite.src }, + { + id: "mapbox-satellite-streets", + name: "卫星街道地图", + img: mapboxSatelliteStreet.src, + }, + { id: "mapbox-streets", name: "街道地图", img: mapboxStreets.src }, + { id: "tianditu-vector", name: "天地图矢量", img: mapboxOutdoors.src }, + { id: "tianditu-image", name: "天地图影像", img: mapboxSatellite.src }, +] as const; const createTileLayer = (url: string, attributions: string) => new TileLayer({ @@ -77,46 +90,37 @@ const createBaseLayerEntries = () => { return [ { - id: "mapbox-light", - name: "默认地图", + ...BASE_LAYER_METADATA[0], layer: lightMapLayer, - img: mapboxLight.src, }, { - id: "mapbox-satellite", - name: "卫星地图", + ...BASE_LAYER_METADATA[1], layer: satelliteLayer, - img: mapboxSatellite.src, }, { - id: "mapbox-satellite-streets", - name: "卫星街道地图", + ...BASE_LAYER_METADATA[2], layer: satelliteStreetsLayer, - img: mapboxSatelliteStreet.src, }, { - id: "mapbox-streets", - name: "街道地图", + ...BASE_LAYER_METADATA[3], layer: streetsLayer, - img: mapboxStreets.src, }, { - id: "tianditu-vector", - name: "天地图矢量", + ...BASE_LAYER_METADATA[4], layer: new Group({ layers: [tiandituVectorLayer, tiandituVectorAnnotationLayer], }), - img: mapboxOutdoors.src, }, { - id: "tianditu-image", - name: "天地图影像", + ...BASE_LAYER_METADATA[5], layer: new Group({ layers: [tiandituImageLayer, tiandituImageAnnotationLayer], }), - img: mapboxSatellite.src, }, - ]; + ].map((entry) => ({ + ...entry, + layer: markMapResourcePersistent(entry.layer), + })); }; const BaseLayers: React.FC = () => { @@ -158,11 +162,7 @@ const BaseLayers: React.FC = () => { }); }; - const baseLayers = useMemo(() => createBaseLayerEntries().map(({ id, name, img }) => ({ - id, - name, - img, - })), []); + const baseLayers = BASE_LAYER_METADATA; const handleQuickSwitch = () => { const nextId = @@ -194,6 +194,15 @@ const BaseLayers: React.FC = () => { }, 300); }; + useEffect( + () => () => { + if (hideTimer.current) { + clearTimeout(hideTimer.current); + } + }, + [], + ); + return (
{ const map = useMap(); @@ -31,14 +32,16 @@ const Scale: React.FC = () => { updateZoomLevel(); // ScaleLine control - const scaleControl = new ScaleLine({ - target: scaleLineRef.current || undefined, - units: "metric", - bar: false, - steps: 4, - text: true, - minWidth: 64, - }); + const scaleControl = markMapResourcePersistent( + new ScaleLine({ + target: scaleLineRef.current || undefined, + units: "metric", + bar: false, + steps: 4, + text: true, + minWidth: 64, + }), + ); map.addControl(scaleControl); return () => { diff --git a/src/components/olmap/core/MapComponent.tsx b/src/components/olmap/core/MapComponent.tsx index b8eca89..2936f48 100644 --- a/src/components/olmap/core/MapComponent.tsx +++ b/src/components/olmap/core/MapComponent.tsx @@ -17,24 +17,21 @@ import MapTools from "./MapTools"; // 导入 DeckLayer import { DeckLayer } from "@utils/layers"; -import VectorTileSource from "ol/source/VectorTile"; -import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; -import MVT from "ol/format/MVT"; -import { FlatStyleLike } from "ol/style/flat"; import { toLonLat } from "ol/proj"; -import { along, bearing, lineString, length, toMercator } from "@turf/turf"; +import { along, bearing, lineString, length } from "@turf/turf"; import { Deck } from "@deck.gl/core"; import { TextLayer } from "@deck.gl/layers"; import { TripsLayer } from "@deck.gl/geo-layers"; import { CollisionFilterExtension } from "@deck.gl/extensions"; -import VectorSource from "ol/source/Vector"; -import GeoJson from "ol/format/GeoJSON"; -import VectorLayer from "ol/layer/Vector"; -import { Icon, Style } from "ol/style.js"; -import { FeatureLike } from "ol/Feature"; -import { Point } from "ol/geom"; import { ContourLayer } from "deck.gl"; import { toM3h } from "@utils/units"; +import { usePathname } from "next/navigation"; +import { + cleanupTransientMapResources, + disposeMapResources, + markMapResourcePersistent, +} from "./mapLifecycle"; +import { createOperationalMapResources } from "./operationalLayers"; interface MapComponentProps { children?: React.ReactNode; @@ -131,6 +128,7 @@ export const useData = () => { }; const MapComponent: React.FC = ({ children }) => { + const pathname = usePathname(); const project = useProject(); const MAP_WORKSPACE = project?.workspace || config.MAP_WORKSPACE; const MAP_EXTENT = (project?.extent || config.MAP_EXTENT) as [ @@ -371,402 +369,20 @@ const MapComponent: React.FC = ({ children }) => { debouncedUpdateDataRef.current = null; }; }, []); - // 配置地图数据源、图层和样式 - const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE; - // 定义 SCADA 图层的样式函数,根据 type 字段选择不同图标 - const scadaStyle = (feature: any) => { - const type = feature.get("type"); - const scadaPressureIcon = "/icons/scada_pressure.svg"; - const scadaFlowIcon = "/icons/scada_flow.svg"; - // 如果 type 不匹配,可以设置默认图标或不显示 - return new Style({ - image: new Icon({ - src: type === "pipe_flow" ? scadaFlowIcon : scadaPressureIcon, - scale: 0.1, // 根据需要调整图标大小 - anchor: [0.5, 0.5], // 图标锚点居中 + const operationalResources = useMemo( + () => + createOperationalMapResources({ + mapUrl: MAP_URL, + workspace: MAP_WORKSPACE, + extent: MAP_EXTENT, + persistent: true, }), - }); - }; - // 定义 reservoirs 图层的样式函数,使用固定图标 - const reservoirStyle = () => { - const reserviorIcon = "/icons/reservior.svg"; - return new Style({ - image: new Icon({ - src: reserviorIcon, - scale: 0.1, // 根据需要调整图标大小 - anchor: [0.5, 0.5], // 图标锚点居中 - }), - }); - }; - // 定义 tanks 图层的样式函数,使用固定图标 - const tankStyle = () => { - const tankIcon = "/icons/tank.svg"; - return new Style({ - image: new Icon({ - src: tankIcon, - scale: 0.1, // 根据需要调整图标大小 - anchor: [0.5, 0.5], // 图标锚点居中 - }), - }); - }; - const valveStyle = { - "icon-src": "/icons/valve.svg", - "icon-scale": 0.1, - }; - // 定义 pumps 图层的样式函数,使用固定图标 - const pumpStyle = function (feature: FeatureLike) { - const styles = []; - const pumpIcon = "/icons/pump.svg"; - - const geometry = feature.getGeometry(); - const lineCoords = - geometry?.getType() === "LineString" - ? (geometry as any).getCoordinates() - : null; - if (geometry) { - const lineCoordsWGS84 = lineCoords.map((coord: []) => { - const [lon, lat] = toLonLat(coord); - return [lon, lat]; - }); - // 计算中点 - const lineStringFeature = lineString(lineCoordsWGS84); - const lineLength = length(lineStringFeature); - const midPoint = along(lineStringFeature, lineLength / 2).geometry - .coordinates; - // 在中点添加 icon 样式 - const midPointMercator = toMercator(midPoint); - styles.push( - new Style({ - geometry: new Point(midPointMercator), - image: new Icon({ - src: pumpIcon, - scale: 0.12, - anchor: [0.5, 0.5], - }), - }), - ); - } - return styles; - }; - // 矢量瓦片数据源和图层 - const junctionSource = new VectorTileSource({ - url: `${MAP_URL}/gwc/service/tms/1.0.0/${MAP_WORKSPACE}:geo_junctions@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`, // 替换为你的 MVT 瓦片服务 URL - format: new MVT(), - projection: "EPSG:3857", - }); - const pipeSource = new VectorTileSource({ - url: `${MAP_URL}/gwc/service/tms/1.0.0/${MAP_WORKSPACE}:geo_pipes@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`, // 替换为你的 MVT 瓦片服务 URL - format: new MVT(), - projection: "EPSG:3857", - }); - const valveSource = new VectorTileSource({ - url: `${MAP_URL}/gwc/service/tms/1.0.0/${MAP_WORKSPACE}:geo_valves@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`, // 替换为你的 MVT 瓦片服务 URL - format: new MVT(), - projection: "EPSG:3857", - }); - const reservoirSource = new VectorSource({ - url: `${MAP_URL}/${MAP_WORKSPACE}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${MAP_WORKSPACE}:geo_reservoirs&outputFormat=application/json`, - format: new GeoJson(), - }); - const pumpSource = new VectorSource({ - url: `${MAP_URL}/${MAP_WORKSPACE}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${MAP_WORKSPACE}:geo_pumps&outputFormat=application/json`, - format: new GeoJson(), - }); - const tankSource = new VectorSource({ - url: `${MAP_URL}/${MAP_WORKSPACE}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${MAP_WORKSPACE}:geo_tanks&outputFormat=application/json`, - format: new GeoJson(), - }); - const scadaSource = new VectorSource({ - url: `${MAP_URL}/${MAP_WORKSPACE}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${MAP_WORKSPACE}:geo_scada&outputFormat=application/json`, - format: new GeoJson(), - }); - - // WebGL 渲染优化显示 - const junctionsLayer = new WebGLVectorTileLayer({ - source: junctionSource as any, // 使用 WebGL 渲染 - style: defaultFlatStyle, - extent: MAP_EXTENT, // 设置图层范围 - maxZoom: 24, - minZoom: 11, - properties: { - name: "节点", // 设置图层名称 - value: "junctions", - type: "point", - properties: [ - // { name: "需求量", value: "demand" }, - { name: "高程", value: "elevation" }, - // 计算属性 - { name: "实际需水量", value: "actual_demand" }, - { name: "水头", value: "total_head" }, - { name: "压力", value: "pressure" }, - { name: "水质", value: "quality" }, - ], - }, - }); - const pipesLayer = new WebGLVectorTileLayer({ - source: pipeSource as any, // 使用 WebGL 渲染 - style: defaultFlatStyle, - extent: MAP_EXTENT, // 设置图层范围 - maxZoom: 24, - minZoom: 11, - properties: { - name: "管道", // 设置图层名称 - value: "pipes", - type: "linestring", - properties: [ - { name: "管径", value: "diameter" }, - // { name: "粗糙度", value: "roughness" }, - // { name: "局部损失", value: "minor_loss" }, - // 计算属性 - { 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 valvesLayer = new WebGLVectorTileLayer({ - source: valveSource as any, - style: valveStyle, - extent: MAP_EXTENT, // 设置图层范围 - maxZoom: 24, - minZoom: 16, - properties: { - name: "阀门", // 设置图层名称 - value: "valves", - type: "linestring", - properties: [], - }, - }); - const reservoirsLayer = new VectorLayer({ - source: reservoirSource, - style: reservoirStyle, - extent: MAP_EXTENT, // 设置图层范围 - maxZoom: 24, - minZoom: 11, - properties: { - name: "水库", // 设置图层名称 - value: "reservoirs", - type: "point", - properties: [], - }, - }); - const pumpsLayer = new VectorLayer({ - source: pumpSource, - style: pumpStyle, - extent: MAP_EXTENT, // 设置图层范围 - maxZoom: 24, - minZoom: 11, - properties: { - name: "水泵", // 设置图层名称 - value: "pumps", - type: "linestring", - properties: [], - }, - }); - const tanksLayer = new VectorLayer({ - source: tankSource, - style: tankStyle, - extent: MAP_EXTENT, // 设置图层范围 - maxZoom: 24, - minZoom: 11, - properties: { - name: "水箱", // 设置图层名称 - value: "tanks", - type: "point", - properties: [], - }, - }); - const scadaLayer = new VectorLayer({ - source: scadaSource, - style: scadaStyle, - extent: MAP_EXTENT, // 设置图层范围 - maxZoom: 24, - minZoom: 11, - properties: { - name: "SCADA", // 设置图层名称 - value: "scada", - type: "point", - properties: [], - }, - }); - - 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; - }; + [MAP_URL, MAP_WORKSPACE, MAP_EXTENT], + ); + const { junctions: junctionSource, pipes: pipeSource } = + operationalResources.sources; + const { junctions: junctionsLayer, pipes: pipesLayer } = + operationalResources.layers; // The map and layer instances are intentionally rebuilt only when workspace or extent changes. useEffect(() => { @@ -775,6 +391,8 @@ const MapComponent: React.FC = ({ children }) => { return; } isDisposingRef.current = false; + const activeJunctionDataIds = junctionDataIds.current; + const activePipeDataIds = pipeDataIds.current; const addTimeout = (callback: () => void, delay: number) => { const timerId = window.setTimeout(() => { @@ -927,58 +545,6 @@ const MapComponent: React.FC = ({ children }) => { // 添加事件监听器 junctionsLayer.on("change:visible", handleJunctionVisibilityChange); pipesLayer.on("change:visible", handlePipeVisibilityChange); - const availableLayers: any[] = []; - config.MAP_AVAILABLE_LAYERS.forEach((layerValue) => { - switch (layerValue) { - case "junctions": - availableLayers.push(junctionsLayer); - break; - case "pipes": - availableLayers.push(pipesLayer); - break; - case "valves": - availableLayers.push(valvesLayer); - break; - case "reservoirs": - availableLayers.push(reservoirsLayer); - break; - case "pumps": - availableLayers.push(pumpsLayer); - break; - case "tanks": - availableLayers.push(tanksLayer); - break; - case "scada": - availableLayers.push(scadaLayer); - break; - } - }); - // 重新排列图层顺序,确保顺序 点>线>面 - availableLayers.sort((a, b) => { - // 明确顺序(点类优先),这里 valves 特殊处理 - const order = [ - "valves", - "junctions", - "scada", - "reservoirs", - "pumps", - "tanks", - "pipes", - ].reverse(); - // 取值时做安全检查,兼容不同写法(properties.value 或 直接 value) - 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); - // 如果未在 order 中找到,放到末尾 - if (ia === -1) ia = order.length; - if (ib === -1) ib = order.length; - return ia - ib; - }); const map = new OlMap({ target: mapRef.current, view: new View({ @@ -986,9 +552,10 @@ const MapComponent: React.FC = ({ children }) => { projection: "EPSG:3857", }), // 图层依面、线、点、标注次序添加 - layers: availableLayers.slice(), + layers: operationalResources.orderedLayers.slice(), controls: [], }); + map.getInteractions().forEach(markMapResourcePersistent); setMap(map); // 恢复上次视图;如果没有则适配 MAP_EXTENT @@ -1072,10 +639,12 @@ const MapComponent: React.FC = ({ children }) => { controller: false, // 由 OpenLayers 控制视图 layers: [], }); - const deckLayer = new DeckLayer(deck, canvasRef.current, { - name: "deckLayer", - value: "deckLayer", - }); + const deckLayer = markMapResourcePersistent( + new DeckLayer(deck, canvasRef.current, { + name: "deckLayer", + value: "deckLayer", + }), + ); deckLayerRef.current = deckLayer; setDeckLayer(deckLayer); map.addLayer(deckLayer); @@ -1101,8 +670,13 @@ const MapComponent: React.FC = ({ children }) => { } deckLayerRef.current = null; setDeckLayer(undefined); - map.setTarget(undefined); - map.dispose(); + // React Strict Mode re-runs effects with the same memoized layer instances. + // Detach and clear them here, but leave final layer/source disposal to GC. + disposeMapResources(map, { disposeLayers: false }); + activeJunctionDataIds.clear(); + activePipeDataIds.clear(); + tileJunctionDataBuffer.current = []; + tilePipeDataBuffer.current = []; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [MAP_WORKSPACE, MAP_EXTENT]); @@ -1117,11 +691,15 @@ const MapComponent: React.FC = ({ children }) => { if (!map || !compareMapRef.current || !compareCanvasRef.current) return; isCompareDisposingRef.current = false; - const availableLayers = createOperationalLayers(); + const compareResources = createOperationalMapResources({ + mapUrl: MAP_URL, + workspace: MAP_WORKSPACE, + extent: MAP_EXTENT, + }); const nextCompareMap = new OlMap({ target: compareMapRef.current, view: map.getView(), - layers: availableLayers.slice(), + layers: compareResources.orderedLayers.slice(), controls: [], }); nextCompareMap.getAllLayers().forEach((layer) => { @@ -1180,8 +758,7 @@ const MapComponent: React.FC = ({ children }) => { compareDeckLayerRef.current = null; setCompareDeckLayer(undefined); setCompareMap(undefined); - nextCompareMap.setTarget(undefined); - nextCompareMap.dispose(); + disposeMapResources(nextCompareMap); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isCompareMode, map]); @@ -1197,6 +774,34 @@ const MapComponent: React.FC = ({ children }) => { }; }, [compareMap, isCompareMode, map]); + useEffect(() => { + setCurrentTime(-1); + setSelectedDate(new Date()); + setSchemeName(""); + setCurrentJunctionCalData([]); + setCurrentPipeCalData([]); + setCompareJunctionCalData([]); + setComparePipeCalData([]); + setCompareMode(false); + setShowJunctionTextLayer(false); + setShowPipeTextLayer(false); + setShowJunctionId(false); + setShowPipeId(false); + setShowContourLayer(false); + setContours([]); + setContourLayerAvailable(false); + setWaterflowLayerAvailable(false); + setShowWaterflowLayer(false); + setForceStyleAutoApplyVersion(0); + + return () => { + if (!map) return; + cleanupTransientMapResources(map); + deckLayerRef.current?.resetSessionLayers(); + operationalResources.resetStyles(); + }; + }, [pathname, map, operationalResources]); + // 当数据变化时,更新 deck.gl 图层 useEffect(() => { const syncDeckOverlay = ( @@ -1348,7 +953,10 @@ const MapComponent: React.FC = ({ children }) => { }) : null; - if (junctionTextLayer && targetDeckLayer.getDeckLayerById("junctionTextLayer")) { + if ( + junctionTextLayer && + targetDeckLayer.getDeckLayerById("junctionTextLayer") + ) { targetDeckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer); } else if (junctionTextLayer) { targetDeckLayer.addDeckLayer(junctionTextLayer); @@ -1358,7 +966,10 @@ const MapComponent: React.FC = ({ children }) => { } else if (pipeTextLayer) { targetDeckLayer.addDeckLayer(pipeTextLayer); } - if (contourLayer && targetDeckLayer.getDeckLayerById("junctionContourLayer")) { + if ( + contourLayer && + targetDeckLayer.getDeckLayerById("junctionContourLayer") + ) { targetDeckLayer.updateDeckLayer("junctionContourLayer", contourLayer); } else if (contourLayer) { targetDeckLayer.addDeckLayer(contourLayer); diff --git a/src/components/olmap/core/mapLifecycle.test.ts b/src/components/olmap/core/mapLifecycle.test.ts new file mode 100644 index 0000000..38c4056 --- /dev/null +++ b/src/components/olmap/core/mapLifecycle.test.ts @@ -0,0 +1,94 @@ +import { + cleanupTransientMapResources, + disposeMapResources, + isMapResourcePersistent, + markMapResourcePersistent, +} from "./mapLifecycle"; + +const createCollection = (items: T[]) => ({ + getArray: () => items, + clear: () => items.splice(0, items.length), +}); + +const createResource = () => { + const properties = new Map(); + return { + get: (key: string) => properties.get(key), + set: (key: string, value: unknown) => properties.set(key, value), + dispose: jest.fn(), + }; +}; + +describe("map lifecycle", () => { + it("preserves shared resources and releases session resources", () => { + const persistentLayer = markMapResourcePersistent(createResource()); + const transientSource = { clear: jest.fn(), dispose: jest.fn() }; + const transientLayer = { + ...createResource(), + getSource: () => transientSource, + }; + const persistentInteraction = markMapResourcePersistent(createResource()); + const transientInteraction = createResource(); + const persistentControl = markMapResourcePersistent(createResource()); + const transientControl = createResource(); + + const layers = [persistentLayer, transientLayer]; + const interactions = [persistentInteraction, transientInteraction]; + const controls = [persistentControl, transientControl]; + const overlays: ReturnType[] = []; + const map = { + getLayers: () => createCollection(layers), + removeLayer: (resource: unknown) => layers.splice(layers.indexOf(resource as never), 1), + getInteractions: () => createCollection(interactions), + removeInteraction: (resource: unknown) => + interactions.splice(interactions.indexOf(resource as never), 1), + getControls: () => createCollection(controls), + removeControl: (resource: unknown) => controls.splice(controls.indexOf(resource as never), 1), + getOverlays: () => createCollection(overlays), + removeOverlay: (resource: unknown) => overlays.splice(overlays.indexOf(resource as never), 1), + } as any; + + cleanupTransientMapResources(map); + + expect(layers).toEqual([persistentLayer]); + expect(interactions).toEqual([persistentInteraction]); + expect(controls).toEqual([persistentControl]); + expect(transientSource.clear).toHaveBeenCalledTimes(1); + expect(transientSource.dispose).toHaveBeenCalledTimes(1); + expect(transientLayer.dispose).toHaveBeenCalledTimes(1); + expect(transientInteraction.dispose).toHaveBeenCalledTimes(1); + expect(transientControl.dispose).toHaveBeenCalledTimes(1); + }); + + it("marks only explicitly shared resources as persistent", () => { + const resource = createResource(); + expect(isMapResourcePersistent(resource)).toBe(false); + markMapResourcePersistent(resource); + expect(isMapResourcePersistent(resource)).toBe(true); + }); + + it("keeps memoized layers reusable across Strict Mode effect cleanup", () => { + const source = { clear: jest.fn(), dispose: jest.fn() }; + const layer = { ...createResource(), getSource: () => source }; + const layers = [layer]; + const interactions: ReturnType[] = []; + const controls: ReturnType[] = []; + const overlays: ReturnType[] = []; + const map = { + getLayers: () => createCollection(layers), + removeLayer: (resource: unknown) => layers.splice(layers.indexOf(resource as never), 1), + getInteractions: () => createCollection(interactions), + getControls: () => createCollection(controls), + getOverlays: () => createCollection(overlays), + setTarget: jest.fn(), + dispose: jest.fn(), + } as any; + + disposeMapResources(map, { disposeLayers: false }); + + expect(source.clear).toHaveBeenCalledTimes(1); + expect(source.dispose).not.toHaveBeenCalled(); + expect(layer.dispose).not.toHaveBeenCalled(); + expect(map.dispose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/olmap/core/mapLifecycle.ts b/src/components/olmap/core/mapLifecycle.ts new file mode 100644 index 0000000..8e6404c --- /dev/null +++ b/src/components/olmap/core/mapLifecycle.ts @@ -0,0 +1,90 @@ +import type { Map as OlMap } from "ol"; + +const RESOURCE_SCOPE_KEY = "tjwater_resource_scope"; +const PERSISTENT_SCOPE = "persistent"; + +type MapResource = { + get?: (key: string) => unknown; + set?: (key: string, value: unknown, silent?: boolean) => void; + dispose?: () => void; +}; + +export const markMapResourcePersistent = (resource: T): T => { + resource.set?.(RESOURCE_SCOPE_KEY, PERSISTENT_SCOPE, true); + return resource; +}; + +export const isMapResourcePersistent = (resource: MapResource) => + resource.get?.(RESOURCE_SCOPE_KEY) === PERSISTENT_SCOPE; + +const removeTransientResources = ( + resources: T[], + remove: (resource: T) => void, +) => { + [...resources].forEach((resource) => { + if (isMapResourcePersistent(resource)) return; + remove(resource); + resource.dispose?.(); + }); +}; + +const releaseLayer = (layer: any, dispose: boolean) => { + const childLayers = layer.getLayers?.().getArray?.(); + if (Array.isArray(childLayers)) { + [...childLayers].forEach((childLayer) => releaseLayer(childLayer, dispose)); + layer.getLayers().clear(); + } + + const source = layer.getSource?.(); + try { + source?.clear?.(); + } catch { + // Some third-party sources do not support explicit cache clearing. + } + if (dispose) { + try { + source?.dispose?.(); + } catch { + // Source may already be disposed by its owning layer. + } + try { + layer.dispose?.(); + } catch { + // Cleanup is deliberately idempotent for overlapping route unmounts. + } + } +}; + +export const cleanupTransientMapResources = (map: OlMap) => { + [...map.getLayers().getArray()].forEach((layer) => { + if (isMapResourcePersistent(layer)) return; + map.removeLayer(layer); + releaseLayer(layer, true); + }); + + removeTransientResources(map.getInteractions().getArray(), (interaction) => + map.removeInteraction(interaction), + ); + removeTransientResources(map.getControls().getArray(), (control) => + map.removeControl(control), + ); + removeTransientResources(map.getOverlays().getArray(), (overlay) => + map.removeOverlay(overlay), + ); +}; + +export const disposeMapResources = ( + map: OlMap, + options: { disposeLayers?: boolean } = {}, +) => { + const disposeLayers = options.disposeLayers ?? true; + [...map.getLayers().getArray()].forEach((layer) => { + map.removeLayer(layer); + releaseLayer(layer, disposeLayers); + }); + map.getInteractions().clear(); + map.getControls().clear(); + map.getOverlays().clear(); + map.setTarget(undefined); + map.dispose(); +}; diff --git a/src/components/olmap/core/operationalLayers.ts b/src/components/olmap/core/operationalLayers.ts new file mode 100644 index 0000000..7f5ecba --- /dev/null +++ b/src/components/olmap/core/operationalLayers.ts @@ -0,0 +1,255 @@ +import { config } from "@/config/config"; +import { along, lineString, length, toMercator } from "@turf/turf"; +import type { FeatureLike } from "ol/Feature"; +import MVT from "ol/format/MVT"; +import { Point } from "ol/geom"; +import type BaseLayer from "ol/layer/Base"; +import VectorLayer from "ol/layer/Vector"; +import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile"; +import { toLonLat } from "ol/proj"; +import GeoJSON from "ol/format/GeoJSON"; +import VectorSource from "ol/source/Vector"; +import VectorTileSource from "ol/source/VectorTile"; +import { Icon, Style } from "ol/style"; +import type { FlatStyleLike } from "ol/style/flat"; + +import { markMapResourcePersistent } from "./mapLifecycle"; + +type MapExtent = [number, number, number, number]; + +type CreateOperationalLayersOptions = { + mapUrl: string; + workspace: string; + extent: MapExtent; + persistent?: boolean; +}; + +const defaultFlatStyle = config.MAP_DEFAULT_STYLE as FlatStyleLike; +const valveStyle = { + "icon-src": "/icons/valve.svg", + "icon-scale": 0.1, +}; + +const createIconStyle = (src: string, scale = 0.1) => + new Style({ image: new Icon({ src, scale, anchor: [0.5, 0.5] }) }); + +const scadaStyle = (feature: FeatureLike) => + createIconStyle( + feature.get("type") === "pipe_flow" + ? "/icons/scada_flow.svg" + : "/icons/scada_pressure.svg", + ); + +const pumpStyle = (feature: FeatureLike) => { + const geometry = feature.getGeometry(); + if (!geometry || geometry.getType() !== "LineString") return []; + + const coordinates = (geometry as any) + .getCoordinates() + .map((coordinate: number[]) => toLonLat(coordinate)); + if (coordinates.length < 2) return []; + + const featureLine = lineString(coordinates); + const midpoint = along(featureLine, length(featureLine) / 2).geometry + .coordinates; + return [ + new Style({ + geometry: new Point(toMercator(midpoint)), + image: new Icon({ + src: "/icons/pump.svg", + scale: 0.12, + anchor: [0.5, 0.5], + }), + }), + ]; +}; + +const pointProperties = [ + { name: "高程", value: "elevation" }, + { name: "实际需水量", value: "actual_demand" }, + { name: "水头", value: "total_head" }, + { name: "压力", value: "pressure" }, + { name: "水质", value: "quality" }, +]; + +const pipeProperties = [ + { 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" }, +]; + +export const createOperationalMapResources = ({ + mapUrl, + workspace, + extent, + persistent = false, +}: CreateOperationalLayersOptions) => { + const vectorTileUrl = (name: string) => + `${mapUrl}/gwc/service/tms/1.0.0/${workspace}:${name}@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`; + const vectorUrl = (name: string) => + `${mapUrl}/${workspace}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${workspace}:${name}&outputFormat=application/json`; + + const sources = { + junctions: new VectorTileSource({ + url: vectorTileUrl("geo_junctions"), + format: new MVT(), + projection: "EPSG:3857", + }), + pipes: new VectorTileSource({ + url: vectorTileUrl("geo_pipes"), + format: new MVT(), + projection: "EPSG:3857", + }), + valves: new VectorTileSource({ + url: vectorTileUrl("geo_valves"), + format: new MVT(), + projection: "EPSG:3857", + }), + reservoirs: new VectorSource({ + url: vectorUrl("geo_reservoirs"), + format: new GeoJSON(), + }), + pumps: new VectorSource({ + url: vectorUrl("geo_pumps"), + format: new GeoJSON(), + }), + tanks: new VectorSource({ + url: vectorUrl("geo_tanks"), + format: new GeoJSON(), + }), + scada: new VectorSource({ + url: vectorUrl("geo_scada"), + format: new GeoJSON(), + }), + }; + + const layers = { + junctions: new WebGLVectorTileLayer({ + source: sources.junctions as any, + style: defaultFlatStyle, + extent, + maxZoom: 24, + minZoom: 11, + properties: { + name: "节点", + value: "junctions", + type: "point", + properties: pointProperties, + }, + }), + pipes: new WebGLVectorTileLayer({ + source: sources.pipes as any, + style: defaultFlatStyle, + extent, + maxZoom: 24, + minZoom: 11, + properties: { + name: "管道", + value: "pipes", + type: "linestring", + properties: pipeProperties, + }, + }), + valves: new WebGLVectorTileLayer({ + source: sources.valves as any, + style: valveStyle, + extent, + maxZoom: 24, + minZoom: 16, + properties: { + name: "阀门", + value: "valves", + type: "linestring", + properties: [], + }, + }), + reservoirs: new VectorLayer({ + source: sources.reservoirs, + style: () => createIconStyle("/icons/reservior.svg"), + extent, + maxZoom: 24, + minZoom: 11, + properties: { + name: "水库", + value: "reservoirs", + type: "point", + properties: [], + }, + }), + pumps: new VectorLayer({ + source: sources.pumps, + style: pumpStyle, + extent, + maxZoom: 24, + minZoom: 11, + properties: { + name: "水泵", + value: "pumps", + type: "linestring", + properties: [], + }, + }), + tanks: new VectorLayer({ + source: sources.tanks, + style: () => createIconStyle("/icons/tank.svg"), + extent, + maxZoom: 24, + minZoom: 11, + properties: { + name: "水箱", + value: "tanks", + type: "point", + properties: [], + }, + }), + scada: new VectorLayer({ + source: sources.scada, + style: scadaStyle, + extent, + maxZoom: 24, + minZoom: 11, + properties: { + name: "SCADA", + value: "scada", + type: "point", + properties: [], + }, + }), + }; + + if (persistent) { + Object.values(layers).forEach(markMapResourcePersistent); + } + + const layerById = layers as Record; + const enabledLayers = new Set(config.MAP_AVAILABLE_LAYERS); + const orderedLayers = [ + "pipes", + "tanks", + "pumps", + "reservoirs", + "scada", + "junctions", + "valves", + ] + .filter((id) => enabledLayers.has(id)) + .map((id) => layerById[id]); + + return { + sources, + layers, + orderedLayers, + resetStyles: () => { + layers.junctions.setStyle(defaultFlatStyle); + layers.pipes.setStyle(defaultFlatStyle); + layers.valves.setStyle(valveStyle); + }, + }; +}; diff --git a/src/utils/layers.ts b/src/utils/layers.ts index c9ecc30..32ed54a 100644 --- a/src/utils/layers.ts +++ b/src/utils/layers.ts @@ -70,6 +70,12 @@ export class DeckLayer extends Layer { this.deck.setProps({ layers }); } + resetSessionLayers(): void { + if (this.isDisposed) return; + this.userVisibility.clear(); + this.deck.setProps({ layers: [] }); + } + // 获取当前图层 getDeckLayers(): any[] { if (this.isDisposed) return [];