perf(map): reuse resources across routes
Build Push and Deploy / docker-image (push) Successful in 54s
Build Push and Deploy / deploy-fallback-log (push) Has been skipped

Preserve standard network layers between map pages while disposing route-owned overlays and controls to prevent memory growth.
This commit is contained in:
2026-07-10 15:31:14 +08:00
parent c6e6e24aab
commit f5e7312e3b
36 changed files with 753 additions and 703 deletions
@@ -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 (
<HealthRiskProvider>
<MapToolbar
queryType="realtime"
hiddenButtons={["style"]}
HistoryPanel={PredictDataPanel}
/>
<Timeline />
<HealthRiskStatistics />
<Box className="absolute bottom-40 right-4 drop-shadow-xl flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
<StyleLegend
layerName="管道"
layerId="health-risk"
property="健康风险"
colors={RAINBOW_COLORS}
type="line"
dimensions={Array(RAINBOW_COLORS.length).fill(2)}
breaks={[0, ...RISK_BREAKS]}
/>
</Box>
</HealthRiskProvider>
);
}
@@ -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 (
<>
<MapToolbar
queryType="scheme"
schemeType="burst_detection"
hiddenButtons={["style"]}
/>
<BurstDetectionPanel />
</>
);
}
@@ -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 (
<>
<MapToolbar
queryType="scheme"
schemeType="burst_location"
hiddenButtons={["style"]}
/>
<BurstLocationPanel />
</>
);
}
@@ -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 (
<>
<MapToolbar
queryType="scheme"
schemeType="burst_analysis"
enableCompare
/>
<BurstPipeAnalysisPanel />
</>
);
}
@@ -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 (
<>
<MapToolbar
queryType="scheme"
schemeType="contaminant_analysis"
enableCompare
/>
<WaterQualityPanel />
</>
);
}
@@ -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 (
<>
<MapToolbar
queryType="scheme"
schemeType="dma_leak_identification"
hiddenButtons={["style"]}
/>
<DMALeakDetectionPanel />
</>
);
}
@@ -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 (
<>
<MapToolbar queryType="scheme" schemeType="flushing_analysis" />
<FlushingAnalysisPanel />
</>
);
}
+9
View File
@@ -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 <MapComponent>{children}</MapComponent>;
}
@@ -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 (
<>
<MapToolbar hiddenButtons={["style"]} />
<MonitoringPlaceOptimizationPanel />
</>
);
}
@@ -1,7 +1,6 @@
"use client"; "use client";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import MapComponent from "@components/olmap/core/MapComponent";
import Timeline from "@components/olmap/core/Controls/Timeline"; import Timeline from "@components/olmap/core/Controls/Timeline";
import MapToolbar from "@components/olmap/core/Controls/Toolbar"; import MapToolbar from "@components/olmap/core/Controls/Toolbar";
@@ -22,8 +21,7 @@ export default function Home() {
}, []); }, []);
return ( return (
<div className="relative w-full h-full overflow-hidden"> <>
<MapComponent>
<MapToolbar queryType="realtime" /> <MapToolbar queryType="realtime" />
<Timeline /> <Timeline />
<SCADADeviceList <SCADADeviceList
@@ -32,7 +30,6 @@ export default function Home() {
selectedDeviceIds={selectedDeviceIds} selectedDeviceIds={selectedDeviceIds}
/> />
<SCADADataPanel deviceIds={selectedDeviceIds} visible={panelVisible} /> <SCADADataPanel deviceIds={selectedDeviceIds} visible={panelVisible} />
</MapComponent> </>
</div>
); );
} }
@@ -1,7 +1,6 @@
"use client"; "use client";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import MapComponent from "@components/olmap/core/MapComponent";
import MapToolbar from "@components/olmap/core/Controls/Toolbar"; import MapToolbar from "@components/olmap/core/Controls/Toolbar";
import SCADADeviceList from "@components/olmap/SCADA/SCADADeviceList"; import SCADADeviceList from "@components/olmap/SCADA/SCADADeviceList";
@@ -21,8 +20,7 @@ export default function Home() {
}, []); }, []);
return ( return (
<div className="relative w-full h-full overflow-hidden"> <>
<MapComponent>
<MapToolbar hiddenButtons={["style"]} /> <MapToolbar hiddenButtons={["style"]} />
<SCADADeviceList <SCADADeviceList
onDeviceClick={handleDeviceClick} onDeviceClick={handleDeviceClick}
@@ -35,7 +33,6 @@ export default function Home() {
visible={panelVisible} visible={panelVisible}
showCleaning={true} showCleaning={true}
/> />
</MapComponent> </>
</div>
); );
} }
@@ -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 (
<div className="relative w-full h-full overflow-hidden">
<HealthRiskProvider>
<MapComponent>
<MapToolbar
queryType="realtime"
hiddenButtons={["style"]}
HistoryPanel={PredictDataPanel}
/>
<Timeline />
<HealthRiskStatistics />
<Box className="absolute bottom-40 right-4 drop-shadow-xl flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
<StyleLegend
layerName="管道"
layerId="health-risk"
property="健康风险"
colors={RAINBOW_COLORS}
type="line"
dimensions={Array(RAINBOW_COLORS.length).fill(2)}
breaks={[0, ...RISK_BREAKS]}
/>
</Box>
</MapComponent>
</HealthRiskProvider>
</div>
);
}
@@ -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 (
<div className="relative h-full w-full overflow-hidden">
<MapComponent>
<MapToolbar queryType="scheme" schemeType="burst_detection" hiddenButtons={["style"]} />
<BurstDetectionPanel />
</MapComponent>
</div>
);
}
@@ -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 (
<div className="relative w-full h-full overflow-hidden">
<MapComponent>
<MapToolbar
queryType="scheme"
schemeType="burst_location"
hiddenButtons={["style"]}
/>
<BurstLocationPanel />
</MapComponent>
</div>
);
}
@@ -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 (
<div className="relative w-full h-full overflow-hidden">
<MapComponent>
<MapToolbar
queryType="scheme"
schemeType="burst_analysis"
enableCompare
/>
<BurstPipeAnalysisPanel />
</MapComponent>
</div>
);
}
@@ -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 (
<div className="relative w-full h-full overflow-hidden">
<MapComponent>
<MapToolbar
queryType="scheme"
schemeType="contaminant_analysis"
enableCompare
/>
<WaterQualityPanel />
</MapComponent>
</div>
);
}
@@ -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 (
<div className="relative w-full h-full overflow-hidden">
<MapComponent>
<MapToolbar
queryType="scheme"
schemeType="dma_leak_identification"
hiddenButtons={["style"]}
/>
<DMALeakDetectionPanel />
</MapComponent>
</div>
);
}
@@ -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 (
<div className="relative w-full h-full overflow-hidden">
<MapComponent>
<MapToolbar queryType="scheme" schemeType="flushing_analysis" />
<FlushingAnalysisPanel />
</MapComponent>
</div>
);
}
@@ -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 (
<div className="relative w-full h-full overflow-hidden">
<MapComponent>
<MapToolbar hiddenButtons={["style"]} />
<MonitoringPlaceOptimizationPanel />
</MapComponent>
</div>
);
}
@@ -14,8 +14,21 @@ import mapboxStreets from "@assets/map/layers/mapbox-streets.png";
import clsx from "clsx"; import clsx from "clsx";
import { MAPBOX_TOKEN, TIANDITU_TOKEN } from "@config/config"; import { MAPBOX_TOKEN, TIANDITU_TOKEN } from "@config/config";
import type { Map as OlMap } from "ol"; import type { Map as OlMap } from "ol";
import { markMapResourcePersistent } from "../mapLifecycle";
const INITIAL_LAYER = "mapbox-light"; 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) => const createTileLayer = (url: string, attributions: string) =>
new TileLayer({ new TileLayer({
@@ -77,46 +90,37 @@ const createBaseLayerEntries = () => {
return [ return [
{ {
id: "mapbox-light", ...BASE_LAYER_METADATA[0],
name: "默认地图",
layer: lightMapLayer, layer: lightMapLayer,
img: mapboxLight.src,
}, },
{ {
id: "mapbox-satellite", ...BASE_LAYER_METADATA[1],
name: "卫星地图",
layer: satelliteLayer, layer: satelliteLayer,
img: mapboxSatellite.src,
}, },
{ {
id: "mapbox-satellite-streets", ...BASE_LAYER_METADATA[2],
name: "卫星街道地图",
layer: satelliteStreetsLayer, layer: satelliteStreetsLayer,
img: mapboxSatelliteStreet.src,
}, },
{ {
id: "mapbox-streets", ...BASE_LAYER_METADATA[3],
name: "街道地图",
layer: streetsLayer, layer: streetsLayer,
img: mapboxStreets.src,
}, },
{ {
id: "tianditu-vector", ...BASE_LAYER_METADATA[4],
name: "天地图矢量",
layer: new Group({ layer: new Group({
layers: [tiandituVectorLayer, tiandituVectorAnnotationLayer], layers: [tiandituVectorLayer, tiandituVectorAnnotationLayer],
}), }),
img: mapboxOutdoors.src,
}, },
{ {
id: "tianditu-image", ...BASE_LAYER_METADATA[5],
name: "天地图影像",
layer: new Group({ layer: new Group({
layers: [tiandituImageLayer, tiandituImageAnnotationLayer], layers: [tiandituImageLayer, tiandituImageAnnotationLayer],
}), }),
img: mapboxSatellite.src,
}, },
]; ].map((entry) => ({
...entry,
layer: markMapResourcePersistent(entry.layer),
}));
}; };
const BaseLayers: React.FC = () => { const BaseLayers: React.FC = () => {
@@ -158,11 +162,7 @@ const BaseLayers: React.FC = () => {
}); });
}; };
const baseLayers = useMemo(() => createBaseLayerEntries().map(({ id, name, img }) => ({ const baseLayers = BASE_LAYER_METADATA;
id,
name,
img,
})), []);
const handleQuickSwitch = () => { const handleQuickSwitch = () => {
const nextId = const nextId =
@@ -194,6 +194,15 @@ const BaseLayers: React.FC = () => {
}, 300); }, 300);
}; };
useEffect(
() => () => {
if (hideTimer.current) {
clearTimeout(hideTimer.current);
}
},
[],
);
return ( return (
<div className="absolute right-17 bottom-11 z-20"> <div className="absolute right-17 bottom-11 z-20">
<div <div
@@ -1,6 +1,7 @@
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useState, useRef } from "react";
import { useMap } from "../MapComponent"; import { useMap } from "../MapComponent";
import { ScaleLine } from "ol/control"; import { ScaleLine } from "ol/control";
import { markMapResourcePersistent } from "../mapLifecycle";
const Scale: React.FC = () => { const Scale: React.FC = () => {
const map = useMap(); const map = useMap();
@@ -31,14 +32,16 @@ const Scale: React.FC = () => {
updateZoomLevel(); updateZoomLevel();
// ScaleLine control // ScaleLine control
const scaleControl = new ScaleLine({ const scaleControl = markMapResourcePersistent(
new ScaleLine({
target: scaleLineRef.current || undefined, target: scaleLineRef.current || undefined,
units: "metric", units: "metric",
bar: false, bar: false,
steps: 4, steps: 4,
text: true, text: true,
minWidth: 64, minWidth: 64,
}); }),
);
map.addControl(scaleControl); map.addControl(scaleControl);
return () => { return () => {
+79 -468
View File
@@ -17,24 +17,21 @@ import MapTools from "./MapTools";
// 导入 DeckLayer // 导入 DeckLayer
import { DeckLayer } from "@utils/layers"; 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 { 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 { Deck } from "@deck.gl/core";
import { TextLayer } from "@deck.gl/layers"; import { TextLayer } from "@deck.gl/layers";
import { TripsLayer } from "@deck.gl/geo-layers"; import { TripsLayer } from "@deck.gl/geo-layers";
import { CollisionFilterExtension } from "@deck.gl/extensions"; 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 { ContourLayer } from "deck.gl";
import { toM3h } from "@utils/units"; import { toM3h } from "@utils/units";
import { usePathname } from "next/navigation";
import {
cleanupTransientMapResources,
disposeMapResources,
markMapResourcePersistent,
} from "./mapLifecycle";
import { createOperationalMapResources } from "./operationalLayers";
interface MapComponentProps { interface MapComponentProps {
children?: React.ReactNode; children?: React.ReactNode;
@@ -131,6 +128,7 @@ export const useData = () => {
}; };
const MapComponent: React.FC<MapComponentProps> = ({ children }) => { const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const pathname = usePathname();
const project = useProject(); const project = useProject();
const MAP_WORKSPACE = project?.workspace || config.MAP_WORKSPACE; const MAP_WORKSPACE = project?.workspace || config.MAP_WORKSPACE;
const MAP_EXTENT = (project?.extent || config.MAP_EXTENT) as [ const MAP_EXTENT = (project?.extent || config.MAP_EXTENT) as [
@@ -371,402 +369,20 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
debouncedUpdateDataRef.current = null; debouncedUpdateDataRef.current = null;
}; };
}, []); }, []);
// 配置地图数据源、图层和样式 const operationalResources = useMemo(
const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE; () =>
// 定义 SCADA 图层的样式函数,根据 type 字段选择不同图标 createOperationalMapResources({
const scadaStyle = (feature: any) => { mapUrl: MAP_URL,
const type = feature.get("type"); workspace: MAP_WORKSPACE,
const scadaPressureIcon = "/icons/scada_pressure.svg"; extent: MAP_EXTENT,
const scadaFlowIcon = "/icons/scada_flow.svg"; persistent: true,
// 如果 type 不匹配,可以设置默认图标或不显示
return new Style({
image: new Icon({
src: type === "pipe_flow" ? scadaFlowIcon : scadaPressureIcon,
scale: 0.1, // 根据需要调整图标大小
anchor: [0.5, 0.5], // 图标锚点居中
}),
});
};
// 定义 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],
}),
}), }),
[MAP_URL, MAP_WORKSPACE, MAP_EXTENT],
); );
} const { junctions: junctionSource, pipes: pipeSource } =
return styles; operationalResources.sources;
}; const { junctions: junctionsLayer, pipes: pipesLayer } =
// 矢量瓦片数据源和图层 operationalResources.layers;
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;
};
// 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(() => {
@@ -775,6 +391,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
return; return;
} }
isDisposingRef.current = false; isDisposingRef.current = false;
const activeJunctionDataIds = junctionDataIds.current;
const activePipeDataIds = pipeDataIds.current;
const addTimeout = (callback: () => void, delay: number) => { const addTimeout = (callback: () => void, delay: number) => {
const timerId = window.setTimeout(() => { const timerId = window.setTimeout(() => {
@@ -927,58 +545,6 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
// 添加事件监听器 // 添加事件监听器
junctionsLayer.on("change:visible", handleJunctionVisibilityChange); junctionsLayer.on("change:visible", handleJunctionVisibilityChange);
pipesLayer.on("change:visible", handlePipeVisibilityChange); 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({ const map = new OlMap({
target: mapRef.current, target: mapRef.current,
view: new View({ view: new View({
@@ -986,9 +552,10 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
projection: "EPSG:3857", projection: "EPSG:3857",
}), }),
// 图层依面、线、点、标注次序添加 // 图层依面、线、点、标注次序添加
layers: availableLayers.slice(), layers: operationalResources.orderedLayers.slice(),
controls: [], controls: [],
}); });
map.getInteractions().forEach(markMapResourcePersistent);
setMap(map); setMap(map);
// 恢复上次视图;如果没有则适配 MAP_EXTENT // 恢复上次视图;如果没有则适配 MAP_EXTENT
@@ -1072,10 +639,12 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
controller: false, // 由 OpenLayers 控制视图 controller: false, // 由 OpenLayers 控制视图
layers: [], layers: [],
}); });
const deckLayer = new DeckLayer(deck, canvasRef.current, { const deckLayer = markMapResourcePersistent(
new DeckLayer(deck, canvasRef.current, {
name: "deckLayer", name: "deckLayer",
value: "deckLayer", value: "deckLayer",
}); }),
);
deckLayerRef.current = deckLayer; deckLayerRef.current = deckLayer;
setDeckLayer(deckLayer); setDeckLayer(deckLayer);
map.addLayer(deckLayer); map.addLayer(deckLayer);
@@ -1101,8 +670,13 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
} }
deckLayerRef.current = null; deckLayerRef.current = null;
setDeckLayer(undefined); setDeckLayer(undefined);
map.setTarget(undefined); // React Strict Mode re-runs effects with the same memoized layer instances.
map.dispose(); // 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [MAP_WORKSPACE, MAP_EXTENT]); }, [MAP_WORKSPACE, MAP_EXTENT]);
@@ -1117,11 +691,15 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
if (!map || !compareMapRef.current || !compareCanvasRef.current) return; if (!map || !compareMapRef.current || !compareCanvasRef.current) return;
isCompareDisposingRef.current = false; isCompareDisposingRef.current = false;
const availableLayers = createOperationalLayers(); const compareResources = createOperationalMapResources({
mapUrl: MAP_URL,
workspace: MAP_WORKSPACE,
extent: MAP_EXTENT,
});
const nextCompareMap = new OlMap({ const nextCompareMap = new OlMap({
target: compareMapRef.current, target: compareMapRef.current,
view: map.getView(), view: map.getView(),
layers: availableLayers.slice(), layers: compareResources.orderedLayers.slice(),
controls: [], controls: [],
}); });
nextCompareMap.getAllLayers().forEach((layer) => { nextCompareMap.getAllLayers().forEach((layer) => {
@@ -1180,8 +758,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
compareDeckLayerRef.current = null; compareDeckLayerRef.current = null;
setCompareDeckLayer(undefined); setCompareDeckLayer(undefined);
setCompareMap(undefined); setCompareMap(undefined);
nextCompareMap.setTarget(undefined); disposeMapResources(nextCompareMap);
nextCompareMap.dispose();
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCompareMode, map]); }, [isCompareMode, map]);
@@ -1197,6 +774,34 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
}; };
}, [compareMap, isCompareMode, map]); }, [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 图层 // 当数据变化时,更新 deck.gl 图层
useEffect(() => { useEffect(() => {
const syncDeckOverlay = ( const syncDeckOverlay = (
@@ -1348,7 +953,10 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
}) })
: null; : null;
if (junctionTextLayer && targetDeckLayer.getDeckLayerById("junctionTextLayer")) { if (
junctionTextLayer &&
targetDeckLayer.getDeckLayerById("junctionTextLayer")
) {
targetDeckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer); targetDeckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer);
} else if (junctionTextLayer) { } else if (junctionTextLayer) {
targetDeckLayer.addDeckLayer(junctionTextLayer); targetDeckLayer.addDeckLayer(junctionTextLayer);
@@ -1358,7 +966,10 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
} else if (pipeTextLayer) { } else if (pipeTextLayer) {
targetDeckLayer.addDeckLayer(pipeTextLayer); targetDeckLayer.addDeckLayer(pipeTextLayer);
} }
if (contourLayer && targetDeckLayer.getDeckLayerById("junctionContourLayer")) { if (
contourLayer &&
targetDeckLayer.getDeckLayerById("junctionContourLayer")
) {
targetDeckLayer.updateDeckLayer("junctionContourLayer", contourLayer); targetDeckLayer.updateDeckLayer("junctionContourLayer", contourLayer);
} else if (contourLayer) { } else if (contourLayer) {
targetDeckLayer.addDeckLayer(contourLayer); targetDeckLayer.addDeckLayer(contourLayer);
@@ -0,0 +1,94 @@
import {
cleanupTransientMapResources,
disposeMapResources,
isMapResourcePersistent,
markMapResourcePersistent,
} from "./mapLifecycle";
const createCollection = <T,>(items: T[]) => ({
getArray: () => items,
clear: () => items.splice(0, items.length),
});
const createResource = () => {
const properties = new Map<string, unknown>();
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<typeof createResource>[] = [];
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<typeof createResource>[] = [];
const controls: ReturnType<typeof createResource>[] = [];
const overlays: ReturnType<typeof createResource>[] = [];
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);
});
});
+90
View File
@@ -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 = <T extends MapResource>(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 = <T extends MapResource>(
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();
};
@@ -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<string, BaseLayer>;
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);
},
};
};
+6
View File
@@ -70,6 +70,12 @@ export class DeckLayer extends Layer {
this.deck.setProps({ layers }); this.deck.setProps({ layers });
} }
resetSessionLayers(): void {
if (this.isDisposed) return;
this.userVisibility.clear();
this.deck.setProps({ layers: [] });
}
// 获取当前图层 // 获取当前图层
getDeckLayers(): any[] { getDeckLayers(): any[] {
if (this.isDisposed) return []; if (this.isDisposed) return [];