Files
TJWaterServer/src/app/OlMap/MapComponent.tsx

1003 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { config } from "@/config/config";
import React, {
createContext,
useContext,
useState,
useEffect,
useRef,
} from "react";
import { Map as OlMap, VectorTile } from "ol";
import View from "ol/View.js";
import "ol/ol.css";
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 { 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";
interface MapComponentProps {
children?: React.ReactNode;
}
interface DataContextType {
currentTime?: number; // 当前时间
setCurrentTime?: React.Dispatch<React.SetStateAction<number>>;
selectedDate?: Date; // 选择的日期
schemeName?: string; // 当前方案名称
setSchemeName?: React.Dispatch<React.SetStateAction<string>>;
setSelectedDate?: React.Dispatch<React.SetStateAction<Date>>;
currentJunctionCalData?: any[]; // 当前计算结果
setCurrentJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>;
currentPipeCalData?: any[]; // 当前计算结果
setCurrentPipeCalData?: React.Dispatch<React.SetStateAction<any[]>>;
pipeData?: any[]; // 管道数据(含坐标)
showJunctionText?: boolean; // 是否显示节点文本
showPipeText?: boolean; // 是否显示管道文本
setShowJunctionTextLayer?: React.Dispatch<React.SetStateAction<boolean>>;
setShowPipeTextLayer?: React.Dispatch<React.SetStateAction<boolean>>;
setShowContourLayer?: React.Dispatch<React.SetStateAction<boolean>>;
// flowAnimation?: React.RefObject<boolean>;
isContourLayerAvailable?: boolean;
setShowWaterflowLayer?: React.Dispatch<React.SetStateAction<boolean>>;
setContourLayerAvailable?: React.Dispatch<React.SetStateAction<boolean>>;
isWaterflowLayerAvailable?: boolean;
setWaterflowLayerAvailable?: React.Dispatch<React.SetStateAction<boolean>>;
junctionText: string;
pipeText: string;
setJunctionText?: React.Dispatch<React.SetStateAction<string>>;
setPipeText?: React.Dispatch<React.SetStateAction<string>>;
setContours?: React.Dispatch<React.SetStateAction<any[]>>;
deckLayer?: DeckLayer;
}
// 跨组件传递
const MapContext = createContext<OlMap | undefined>(undefined);
const DataContext = createContext<DataContextType | undefined>(undefined);
const MAP_EXTENT = config.MAP_EXTENT as [number, number, number, number];
const MAP_URL = config.MAP_URL;
const MAP_WORKSPACE = config.MAP_WORKSPACE;
const MAP_VIEW_STORAGE_KEY = `${MAP_WORKSPACE}_map_view`; // 持久化 key
// 添加防抖函数
function debounce<F extends (...args: any[]) => any>(func: F, waitFor: number) {
let timeout: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<F>): void => {
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(() => func(...args), waitFor);
};
}
export const useMap = () => {
return useContext(MapContext);
};
export const useData = () => {
return useContext(DataContext);
};
const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
const mapRef = useRef<HTMLDivElement | null>(null);
const deckLayerRef = useRef<DeckLayer | null>(null);
const [map, setMap] = useState<OlMap>();
const [deckLayer, setDeckLayer] = useState<DeckLayer>();
// currentCalData 用于存储当前计算结果
const [currentTime, setCurrentTime] = useState<number>(-1); // 默认选择当前时间
// const [selectedDate, setSelectedDate] = useState<Date>(new Date("2025-9-17"));
const [selectedDate, setSelectedDate] = useState<Date>(new Date()); // 默认今天
const [schemeName, setSchemeName] = useState<string>(""); // 当前方案名称
// 记录 id、对应属性的计算值
const [currentJunctionCalData, setCurrentJunctionCalData] = useState<any[]>(
[]
);
const [currentPipeCalData, setCurrentPipeCalData] = useState<any[]>([]);
// junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染
// currentJunctionCalData 和 currentPipeCalData 变化时会新增并更新 junctionData 和 pipeData 的计算属性值
const [junctionData, setJunctionDataState] = useState<any[]>([]);
const [pipeData, setPipeDataState] = useState<any[]>([]);
const junctionDataIds = useRef(new Set<string>());
const pipeDataIds = useRef(new Set<string>());
const tileJunctionDataBuffer = useRef<any[]>([]);
const tilePipeDataBuffer = useRef<any[]>([]);
const [showJunctionTextLayer, setShowJunctionTextLayer] = useState(false); // 控制节点文本图层显示
const [showPipeTextLayer, setShowPipeTextLayer] = useState(false); // 控制管道文本图层显示
const [showContourLayer, setShowContourLayer] = useState(false); // 控制等高线图层显示
const [junctionText, setJunctionText] = useState("pressure");
const [pipeText, setPipeText] = useState("flow");
const [contours, setContours] = useState<any[]>([]);
const flowAnimation = useRef(false); // 添加动画控制标志
const [isContourLayerAvailable, setContourLayerAvailable] = useState(false); // 控制等高线图层显示
const [isWaterflowLayerAvailable, setWaterflowLayerAvailable] =
useState(true); // 控制等高线图层显示
const [showWaterflowLayer, setShowWaterflowLayer] = useState(false); // 控制等高线图层显示
const [currentZoom, setCurrentZoom] = useState(11); // 当前缩放级别
// 防抖更新函数
const debouncedUpdateData = useRef(
debounce(() => {
if (tileJunctionDataBuffer.current.length > 0) {
setJunctionData(tileJunctionDataBuffer.current);
tileJunctionDataBuffer.current = [];
}
if (tilePipeDataBuffer.current.length > 0) {
setPipeData(tilePipeDataBuffer.current);
tilePipeDataBuffer.current = [];
}
}, 100)
);
const setJunctionData = (newData: any[]) => {
const uniqueNewData = newData.filter((item) => {
if (!item || !item.id) return false;
if (!junctionDataIds.current.has(item.id)) {
junctionDataIds.current.add(item.id);
return true;
}
return false;
});
if (uniqueNewData.length > 0) {
setJunctionDataState((prev) => prev.concat(uniqueNewData));
}
};
const setPipeData = (newData: any[]) => {
const uniqueNewData = newData.filter((item) => {
if (!item || !item.id) return false;
if (!pipeDataIds.current.has(item.id)) {
pipeDataIds.current.add(item.id);
return true;
}
return false;
});
if (uniqueNewData.length > 0) {
setPipeDataState((prev) => prev.concat(uniqueNewData));
}
};
// 配置地图数据源、图层和样式
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], // 图标锚点居中
}),
});
};
// 定义 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: "actualdemand" },
{ name: "水头", value: "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: "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: extent, // 设置图层范围
maxZoom: 24,
minZoom: 11,
properties: {
name: "SCADA", // 设置图层名称
value: "scada",
type: "point",
properties: [],
},
});
useEffect(() => {
if (!mapRef.current) return;
// 缓存 junction、pipe 数据,提供给 deck.gl 提供坐标供标签显示
junctionSource.on("tileloadend", (event) => {
try {
if (event.tile instanceof VectorTile) {
const renderFeatures = event.tile.getFeatures();
const data = new Map();
renderFeatures.forEach((renderFeature) => {
const props = renderFeature.getProperties();
const featureId = props.id;
if (featureId && !junctionDataIds.current.has(featureId)) {
const geometry = renderFeature.getGeometry();
if (geometry) {
const coordinates = geometry.getFlatCoordinates();
const coordWGS84 = toLonLat(coordinates);
data.set(featureId, {
id: featureId,
position: coordWGS84,
elevation: props.elevation || 0,
demand: props.demand || 0,
});
}
}
});
const uniqueData = Array.from(data.values());
if (uniqueData.length > 0) {
uniqueData.forEach((item) =>
tileJunctionDataBuffer.current.push(item)
);
debouncedUpdateData.current();
}
}
} catch (error) {
console.error("Junction tile load error:", error);
}
});
pipeSource.on("tileloadend", (event) => {
try {
if (event.tile instanceof VectorTile) {
const renderFeatures = event.tile.getFeatures();
const data = new Map();
renderFeatures.forEach((renderFeature) => {
try {
const props = renderFeature.getProperties();
const featureId = props.id;
if (featureId && !pipeDataIds.current.has(featureId)) {
const geometry = renderFeature.getGeometry();
if (geometry) {
const flatCoordinates = geometry.getFlatCoordinates();
const stride = geometry.getStride(); // 获取步长,通常为 2
// 重建为 LineString GeoJSON 格式的 coordinates: [[x1, y1], [x2, y2], ...]
const lineCoords = [];
for (let i = 0; i < flatCoordinates.length; i += stride) {
lineCoords.push([
flatCoordinates[i],
flatCoordinates[i + 1],
]);
}
const lineCoordsWGS84 = lineCoords.map((coord) => {
const [lon, lat] = toLonLat(coord);
return [lon, lat];
});
// 添加验证:确保至少有 2 个坐标点
if (lineCoordsWGS84.length < 2) return; // 跳过此特征
// 计算中点
const lineStringFeature = lineString(lineCoordsWGS84);
const lineLength = length(lineStringFeature);
const midPoint = along(lineStringFeature, lineLength / 2)
.geometry.coordinates;
// 计算角度
const prevPoint = along(lineStringFeature, lineLength * 0.49)
.geometry.coordinates;
const nextPoint = along(lineStringFeature, lineLength * 0.51)
.geometry.coordinates;
let lineAngle = bearing(prevPoint, nextPoint);
lineAngle = -lineAngle + 90;
if (lineAngle < -90 || lineAngle > 90) {
lineAngle += 180;
}
// 计算时间戳(可选)
const numSegments = lineCoordsWGS84.length - 1;
const timestamps = [0];
if (numSegments > 0) {
for (let i = 1; i <= numSegments; i++) {
timestamps.push((i / numSegments) * 10);
}
}
data.set(featureId, {
id: featureId,
diameter: props.diameter || 0,
path: lineCoordsWGS84, // 使用重建后的坐标
position: midPoint,
angle: lineAngle,
timestamps,
});
}
}
} catch (geomError) {
console.error("Geometry calculation error:", geomError);
}
});
const uniqueData = Array.from(data.values());
if (uniqueData.length > 0) {
uniqueData.forEach((item) => tilePipeDataBuffer.current.push(item));
debouncedUpdateData.current();
}
}
} catch (error) {
console.error("Pipe tile load error:", error);
}
});
// 监听 junctionsLayer 的 visible 变化
const handleJunctionVisibilityChange = () => {
const isVisible = junctionsLayer.getVisible();
setShowJunctionTextLayer(isVisible);
};
// 监听 pipesLayer 的 visible 变化
const handlePipeVisibilityChange = () => {
const isVisible = pipesLayer.getVisible();
setShowPipeTextLayer(isVisible);
};
// 添加事件监听器
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({
maxZoom: 24,
projection: "EPSG:3857",
}),
// 图层依面、线、点、标注次序添加
layers: availableLayers.slice(),
controls: [],
});
setMap(map);
// 恢复上次视图;如果没有则适配 MAP_EXTENT
try {
const stored = localStorage.getItem(MAP_VIEW_STORAGE_KEY);
if (stored) {
const viewState = JSON.parse(stored);
if (
viewState &&
Array.isArray(viewState.center) &&
viewState.center.length === 2 &&
typeof viewState.zoom === "number" &&
viewState.zoom >= 11 &&
viewState.zoom <= 24
) {
map.getView().setCenter(viewState.center);
map.getView().setZoom(viewState.zoom);
} else {
map.getView().fit(MAP_EXTENT, {
padding: [50, 50, 50, 50],
duration: 1000,
});
}
} else {
map.getView().fit(MAP_EXTENT, {
padding: [50, 50, 50, 50],
duration: 1000,
});
}
} catch (err) {
console.warn("Restore map view failed", err);
map.getView().fit(MAP_EXTENT, {
padding: [50, 50, 50, 50],
duration: 1000,
});
}
// 持久化视图(中心点 + 缩放),防抖写入 localStorage
const persistView = debounce(() => {
try {
const view = map.getView();
const center = view.getCenter();
const zoom = view.getZoom();
if (center && typeof zoom === "number") {
localStorage.setItem(
MAP_VIEW_STORAGE_KEY,
JSON.stringify({ center, zoom })
);
}
} catch (err) {
console.warn("Save map view failed", err);
}
}, 250);
// 监听缩放变化并持久化,同时更新 currentZoom
const handleViewChange = () => {
setTimeout(() => {
const zoom = map.getView().getZoom() || 0;
setCurrentZoom(zoom);
persistView();
}, 250);
};
map.getView().on("change", handleViewChange);
// 初始化当前缩放级别并强制触发瓦片加载
setTimeout(() => {
const initialZoom = map.getView().getZoom() || 11;
setCurrentZoom(initialZoom);
// 强制触发地图渲染,让瓦片加载事件触发
map.render();
}, 100);
// 初始化 deck.gl
const deck = new Deck({
initialViewState: {
longitude: 0,
latitude: 0,
zoom: 1,
},
canvas: "deck-canvas",
controller: false, // 由 OpenLayers 控制视图
layers: [],
});
const deckLayer = new DeckLayer(deck, {
name: "deckLayer",
value: "deckLayer",
});
deckLayerRef.current = deckLayer;
setDeckLayer(deckLayer);
map.addLayer(deckLayer);
// 清理函数
return () => {
junctionsLayer.un("change:visible", handleJunctionVisibilityChange);
pipesLayer.un("change:visible", handlePipeVisibilityChange);
map.setTarget(undefined);
map.dispose();
deck.finalize();
};
}, []);
// 当数据变化时,更新 deck.gl 图层
useEffect(() => {
const deckLayer = deckLayerRef.current;
if (!deckLayer) return; // 如果 deck 实例还未创建,则退出
if (!junctionData.length) return;
if (!pipeData.length) return;
const junctionTextLayer = new TextLayer({
id: "junctionTextLayer",
name: "节点文字",
zIndex: 10,
data: junctionData,
getPosition: (d: any) => d.position,
fontFamily: "Monaco, monospace",
getText: (d: any) => {
if (!d[junctionText]) return "";
const value = (d[junctionText] as number).toFixed(3);
// 根据属性类型添加符号前缀
const prefix =
{
pressure: "P:",
head: "H:",
quality: "Q:",
actualdemand: "D:",
}[junctionText] || "";
return `${prefix}${value}`;
},
getSize: 14,
fontWeight: "bold",
getColor: [33, 37, 41], // 深灰色,在灰白背景上清晰可见
getAngle: 0,
getTextAnchor: "middle",
getAlignmentBaseline: "center",
getPixelOffset: [0, -10],
visible: showJunctionTextLayer && currentZoom >= 15 && currentZoom <= 24,
extensions: [new CollisionFilterExtension()],
collisionTestProps: {
sizeScale: 3,
},
characterSet: "auto",
fontSettings: {
sdf: true,
fontSize: 64,
buffer: 6,
},
// outlineWidth: 3,
// outlineColor: [255, 255, 255, 220],
});
const pipeTextLayer = new TextLayer({
id: "pipeTextLayer",
name: "管道文字",
zIndex: 10,
data: pipeData,
getPosition: (d: any) => d.position,
fontFamily: "Monaco, monospace",
getText: (d: any) => {
if (!d[pipeText]) return "";
const value = Math.abs(d[pipeText] as number).toFixed(3);
// 根据属性类型添加符号前缀
const prefix =
{
flow: "F:",
velocity: "V:",
headloss: "HL:",
diameter: "D:",
friction: "FR:",
}[pipeText] || "";
return `${prefix}${value}`;
},
getSize: 14,
fontWeight: "bold",
getColor: [33, 37, 41], // 深灰色
getAngle: (d: any) => d.angle || 0,
getPixelOffset: [0, -8],
getTextAnchor: "middle",
getAlignmentBaseline: "bottom",
visible: showPipeTextLayer && currentZoom >= 15 && currentZoom <= 24,
extensions: [new CollisionFilterExtension()],
collisionTestProps: {
sizeScale: 3,
},
characterSet: "auto",
fontSettings: {
sdf: true,
fontSize: 64,
buffer: 6,
},
// outlineWidth: 3,
// outlineColor: [255, 255, 255, 220],
});
const contourLayer = new ContourLayer({
id: "junctionContourLayer",
name: "等值线",
data: junctionData,
aggregation: "MEAN",
cellSize: 600,
strokeWidth: 0,
contours: contours,
getPosition: (d) => d.position,
getWeight: (d: any) =>
(d[junctionText] as number) < 0 ? 0 : (d[junctionText] as number),
opacity: 1,
visible: showContourLayer && currentZoom >= 11 && currentZoom <= 24,
});
if (deckLayer.getDeckLayerById("junctionTextLayer")) {
// 传入完整 layer 实例以保证 clone/替换时保留 layer 类型和方法
deckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer);
} else {
deckLayer.addDeckLayer(junctionTextLayer);
}
if (deckLayer.getDeckLayerById("pipeTextLayer")) {
deckLayer.updateDeckLayer("pipeTextLayer", pipeTextLayer);
} else {
deckLayer.addDeckLayer(pipeTextLayer);
}
if (deckLayer.getDeckLayerById("junctionContourLayer")) {
deckLayer.updateDeckLayer("junctionContourLayer", contourLayer);
} else {
deckLayer.addDeckLayer(contourLayer);
}
}, [
junctionData,
pipeData,
currentZoom,
showJunctionTextLayer,
showPipeTextLayer,
showContourLayer,
junctionText,
pipeText,
contours,
]);
// 控制流动动画开关
useEffect(() => {
if (pipeText === "flow" && currentPipeCalData.length > 0) {
flowAnimation.current = true;
} else {
flowAnimation.current = false;
}
const deckLayer = deckLayerRef.current;
if (!deckLayer) return; // 如果 deck 实例还未创建,则退出
let animationFrameId: number; // 保存 requestAnimationFrame 的 ID
// 动画循环
const animate = () => {
if (!flowAnimation.current) {
try {
deckLayer.removeDeckLayer("waterflowLayer");
} catch (error) {
console.error("Error in animation loop:", error);
}
return;
}
// 动画总时长(秒)
if (pipeData.length === 0) {
animationFrameId = requestAnimationFrame(animate);
return;
}
const animationDuration = 10;
// 缓冲时间(秒)
const bufferTime = 2;
// 完整循环周期
const loopLength = animationDuration + bufferTime;
// 确保时间范围与你的时间戳数据匹配
const currentTime = (Date.now() / 1000) % loopLength; // (0,12) 之间循环
// console.log("Current Time:", currentTime);
const waterflowLayer = new TripsLayer({
id: "waterflowLayer",
name: "水流",
data: pipeData,
getPath: (d) => d.path,
getTimestamps: (d) => {
return d.timestamps; // 这些应该是与 currentTime 匹配的数值
},
getColor: [0, 220, 255],
opacity: 0.8,
visible:
isWaterflowLayerAvailable &&
showWaterflowLayer &&
flowAnimation.current &&
currentZoom >= 12 &&
currentZoom <= 24,
widthMinPixels: 5,
jointRounded: true, // 拐角变圆
// capRounded: true, // 端点变圆
trailLength: 2, // 水流尾迹淡出时间
currentTime: currentTime,
});
// if (deckLayer.getDeckLayerById("waterflowLayer")) {
// deckLayer.updateDeckLayer("waterflowLayer", waterflowLayer);
// } else {
// deckLayer.addDeckLayer(waterflowLayer);
// }
// 继续请求动画帧,每帧执行一次函数
animationFrameId = requestAnimationFrame(animate);
};
animate();
// 清理函数:取消动画帧
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
}
};
}, [
currentZoom,
currentPipeCalData,
pipeText,
pipeData.length,
isWaterflowLayerAvailable,
showWaterflowLayer,
]);
// 计算值更新时,更新 junctionData 和 pipeData
useEffect(() => {
const junctionProperties = junctionText;
const pipeProperties = pipeText;
// 将 nodeRecords 转换为 Map 以提高查找效率
const nodeMap: Map<string, any> = new Map(
currentJunctionCalData.map((r: any) => [r.ID, r])
);
// 将 linkRecords 转换为 Map 以提高查找效率
const linkMap: Map<string, any> = new Map(
currentPipeCalData.map((r: any) => [r.ID, r])
);
if (nodeMap.size > 0) {
// 更新junctionData
setJunctionDataState((prev: any[]) =>
prev.map((j) => {
const record = nodeMap.get(j.id);
if (record) {
return {
...j,
[junctionProperties]: record.value,
};
}
return j;
})
);
}
if (linkMap.size > 0) {
// 更新pipeData
setPipeDataState((prev: any[]) =>
prev.map((p) => {
const record = linkMap.get(p.id);
if (record) {
return {
...p,
flowFlag: pipeProperties === "flow" && record.value < 0 ? -1 : 1,
path:
pipeProperties === "flow" && record.value < 0 && p.flowFlag > 0
? p.path.slice().reverse()
: p.path,
// 流量数值
[pipeProperties]:
pipeProperties === "flow"
? Math.abs(record.value)
: record.value,
};
}
return p;
})
);
}
}, [currentJunctionCalData, currentPipeCalData]);
return (
<>
<DataContext.Provider
value={{
currentTime,
setCurrentTime,
selectedDate,
setSelectedDate,
schemeName,
setSchemeName,
currentJunctionCalData,
setCurrentJunctionCalData,
currentPipeCalData,
setCurrentPipeCalData,
pipeData,
setShowJunctionTextLayer,
setShowPipeTextLayer,
setShowContourLayer,
// flowAnimation,
isContourLayerAvailable,
setContourLayerAvailable,
isWaterflowLayerAvailable,
setWaterflowLayerAvailable,
setShowWaterflowLayer,
setJunctionText,
setPipeText,
junctionText,
pipeText,
setContours,
deckLayer,
}}
>
<MapContext.Provider value={map}>
<div className="relative w-full h-full">
<div ref={mapRef} className="w-full h-full"></div>
<MapTools />
{children}
</div>
<canvas id="deck-canvas" />
</MapContext.Provider>
</DataContext.Provider>
</>
);
};
export default MapComponent;