915 lines
31 KiB
TypeScript
915 lines
31 KiB
TypeScript
"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";
|
||
|
||
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[]>>;
|
||
showJunctionText?: boolean; // 是否显示节点文本
|
||
showPipeText?: boolean; // 是否显示管道文本
|
||
setShowJunctionText?: React.Dispatch<React.SetStateAction<boolean>>;
|
||
setShowPipeText?: React.Dispatch<React.SetStateAction<boolean>>;
|
||
setShowContourLayer?: React.Dispatch<React.SetStateAction<boolean>>;
|
||
junctionText: string;
|
||
pipeText: string;
|
||
setJunctionText?: React.Dispatch<React.SetStateAction<string>>;
|
||
setPipeText?: React.Dispatch<React.SetStateAction<string>>;
|
||
scadaData?: any[]; // SCADA 数据
|
||
}
|
||
|
||
// 跨组件传递
|
||
const MapContext = createContext<OlMap | undefined>(undefined);
|
||
const DataContext = createContext<DataContextType | undefined>(undefined);
|
||
|
||
const MAP_EXTENT = config.MAP_EXTENT;
|
||
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 deckRef = useRef<Deck | null>(null);
|
||
const deckLayerRef = useRef<DeckLayer | null>(null);
|
||
|
||
const [map, setMap] = useState<OlMap>();
|
||
// 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>(""); // 当前方案名称
|
||
|
||
const [currentJunctionCalData, setCurrentJunctionCalData] = useState<any[]>(
|
||
[]
|
||
);
|
||
const [currentPipeCalData, setCurrentPipeCalData] = useState<any[]>([]);
|
||
// junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染
|
||
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 [showJunctionText, setShowJunctionText] = useState(false); // 控制节点文本显示
|
||
const [showPipeText, setShowPipeText] = useState(false); // 控制管道文本显示
|
||
const [showJunctionTextLayer, setShowJunctionTextLayer] = useState(true); // 控制节点文本图层显示
|
||
const [showPipeTextLayer, setShowPipeTextLayer] = useState(true); // 控制管道文本图层显示
|
||
const [showContourLayer, setShowContourLayer] = useState(true); // 控制等高线图层显示
|
||
const [junctionText, setJunctionText] = useState("pressure");
|
||
const [pipeText, setPipeText] = useState("flow");
|
||
const flowAnimation = useRef(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, ...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, ...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) {
|
||
tileJunctionDataBuffer.current.push(...uniqueData);
|
||
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) {
|
||
tilePipeDataBuffer.current.push(...uniqueData);
|
||
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],
|
||
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"
|
||
) {
|
||
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();
|
||
}, 0);
|
||
};
|
||
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: [],
|
||
});
|
||
deckRef.current = deck;
|
||
const deckLayer = new DeckLayer(deck, {
|
||
name: "deckLayer",
|
||
value: "deckLayer",
|
||
});
|
||
deckLayerRef.current = 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;
|
||
console.log(pipeData);
|
||
console.log(pipeText);
|
||
const junctionTextLayer = new TextLayer({
|
||
id: "junctionTextLayer",
|
||
zIndex: 10,
|
||
data: showJunctionText ? junctionData : [],
|
||
getPosition: (d: any) => d.position,
|
||
fontFamily: "Monaco, monospace",
|
||
getText: (d: any) =>
|
||
d[junctionText] ? (d[junctionText] as number).toFixed(3) : "",
|
||
getSize: 18,
|
||
fontWeight: "bold",
|
||
getColor: [0, 0, 0],
|
||
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: 10,
|
||
// outlineColor: [242, 244, 246, 255],
|
||
});
|
||
const pipeTextLayer = new TextLayer({
|
||
id: "pipeTextLayer",
|
||
zIndex: 10,
|
||
data: showPipeText ? pipeData : [],
|
||
getPosition: (d: any) => d.position,
|
||
fontFamily: "Monaco, monospace",
|
||
getText: (d: any) =>
|
||
d[pipeText] ? Math.abs(d[pipeText] as number).toFixed(3) : "",
|
||
getSize: 18,
|
||
fontWeight: "bold",
|
||
getColor: [0, 0, 0],
|
||
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: 10,
|
||
// outlineColor: [242, 244, 246, 255],
|
||
});
|
||
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);
|
||
}
|
||
console.log(deckLayer.getDeckLayers());
|
||
}, [
|
||
junctionData,
|
||
pipeData,
|
||
currentZoom,
|
||
showJunctionText,
|
||
showPipeText,
|
||
showJunctionTextLayer,
|
||
showPipeTextLayer,
|
||
showContourLayer,
|
||
junctionText,
|
||
pipeText,
|
||
]);
|
||
// 控制流动动画开关
|
||
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 (!deckRef.current || !flowAnimation.current) 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",
|
||
data: pipeData,
|
||
getPath: (d) => d.path,
|
||
getTimestamps: (d) => {
|
||
return d.timestamps; // 这些应该是与 currentTime 匹配的数值
|
||
},
|
||
getColor: [0, 220, 255],
|
||
opacity: 0.8,
|
||
visible:
|
||
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]);
|
||
|
||
// 计算值更新时,更新 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])
|
||
);
|
||
|
||
// 更新junctionData
|
||
setJunctionDataState((prev: any[]) =>
|
||
prev.map((j) => {
|
||
const record = nodeMap.get(j.id);
|
||
if (record) {
|
||
return {
|
||
...j,
|
||
[junctionProperties]: record.value,
|
||
};
|
||
}
|
||
return j;
|
||
})
|
||
);
|
||
|
||
// 更新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].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,
|
||
setShowJunctionText,
|
||
setShowPipeText,
|
||
setShowContourLayer,
|
||
setJunctionText,
|
||
setPipeText,
|
||
showJunctionText,
|
||
showPipeText,
|
||
junctionText,
|
||
pipeText,
|
||
}}
|
||
>
|
||
<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;
|