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

622 lines
21 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";
import { Layer } from "ol/layer"; // 保留导入,但用于继承
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 { center } from "@turf/center";
import { bearing } 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";
interface MapComponentProps {
children?: React.ReactNode;
}
interface DataContextType {
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>>;
junctionText: string;
pipeText: string;
setJunctionText?: React.Dispatch<React.SetStateAction<string>>;
setPipeText?: React.Dispatch<React.SetStateAction<string>>;
}
// 创建自定义Layer类来包装deck.gl
class DeckLayer extends Layer {
private deck: Deck;
constructor(deckInstance: Deck) {
super({});
this.deck = deckInstance;
}
render(frameState: any): HTMLElement {
const { size, viewState } = frameState;
const [width, height] = size;
const [longitude, latitude] = toLonLat(viewState.center);
const zoom = viewState.zoom - 1; // 调整 zoom 以匹配
const bearing = (-viewState.rotation * 180) / Math.PI;
const deckViewState = { bearing, longitude, latitude, zoom };
this.deck.setProps({ width, height, viewState: deckViewState });
this.deck.redraw();
// 返回deck.gl的canvas元素
return document.getElementById("deck-canvas") as HTMLElement;
}
}
// 跨组件传递
const MapContext = createContext<OlMap | undefined>(undefined);
const DataContext = createContext<DataContextType | undefined>(undefined);
const extent = config.mapExtent;
const mapUrl = config.mapUrl;
// 添加防抖函数
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 [map, setMap] = useState<OlMap>();
// currentCalData 用于存储当前计算结果
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 [junctionText, setJunctionText] = useState("pressure");
const [pipeText, setPipeText] = useState("flow");
const flowAnimation = useRef(false); // 添加动画控制标志
const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别
// 防抖更新函数
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 = {
"stroke-width": 3,
"stroke-color": "rgba(51, 153, 204, 0.9)",
"circle-fill-color": "rgba(255,255,255,0.4)",
"circle-stroke-color": "rgba(255,255,255,0.9)",
"circle-radius": [
"interpolate",
["linear"],
["zoom"],
12,
1, // 在缩放级别 12 时,圆形半径为 1px
24,
12, // 在缩放级别 24 时,圆形半径为 12px
],
};
// 矢量瓦片数据源和图层
const junctionSource = new VectorTileSource({
url: `${mapUrl}/gwc/service/tms/1.0.0/TJWater:geo_junctions_mat@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`, // 替换为你的 MVT 瓦片服务 URL
format: new MVT(),
projection: "EPSG:3857",
});
const pipeSource = new VectorTileSource({
url: `${mapUrl}/gwc/service/tms/1.0.0/TJWater:geo_pipes_mat@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`, // 替换为你的 MVT 瓦片服务 URL
format: new MVT(),
projection: "EPSG:3857",
});
// WebGL 渲染优化显示
const junctionLayer = new WebGLVectorTileLayer({
source: junctionSource as any, // 使用 WebGL 渲染
style: defaultFlatStyle,
extent: extent, // 设置图层范围
maxZoom: 24,
minZoom: 12,
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 pipeLayer = new WebGLVectorTileLayer({
source: pipeSource as any, // 使用 WebGL 渲染
style: defaultFlatStyle,
extent: extent, // 设置图层范围
maxZoom: 24,
minZoom: 12,
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" },
],
},
});
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];
});
// 计算中点
const midPoint = center({
type: "LineString",
coordinates: lineCoordsWGS84,
}).geometry.coordinates;
// 计算角度
let lineAngle = bearing(
lineCoordsWGS84[0],
lineCoordsWGS84[lineCoordsWGS84.length - 1]
);
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);
}
});
// 更新标签可见性状态
// 监听 junctionLayer 的 visible 变化
const handleJunctionVisibilityChange = () => {
const isVisible = junctionLayer.getVisible();
setShowJunctionTextLayer(isVisible);
};
// 监听 pipeLayer 的 visible 变化
const handlePipeVisibilityChange = () => {
const isVisible = pipeLayer.getVisible();
setShowPipeTextLayer(isVisible);
};
// 添加事件监听器
junctionLayer.on("change:visible", handleJunctionVisibilityChange);
pipeLayer.on("change:visible", handlePipeVisibilityChange);
const map = new OlMap({
target: mapRef.current,
view: new View({
projection: "EPSG:3857",
}),
// 图层依面、线、点、标注次序添加
layers: [pipeLayer, junctionLayer],
controls: [],
});
setMap(map);
map.getView().fit(extent, {
padding: [50, 50, 50, 50], // 添加一些内边距
duration: 1000, // 动画持续时间
});
// 监听缩放变化
map.getView().on("change", () => {
setTimeout(() => {
const zoom = map.getView().getZoom() || 0;
setCurrentZoom(zoom);
}, 0);
});
// 初始化 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);
// deckLayer.setZIndex(1000); // 确保在最上层
map.addLayer(deckLayer);
// 清理函数
return () => {
junctionLayer.un("change:visible", handleJunctionVisibilityChange);
pipeLayer.un("change:visible", handlePipeVisibilityChange);
map.setTarget(undefined);
map.dispose();
deck.finalize();
};
}, []);
// 当数据变化时,更新 deck.gl 图层
useEffect(() => {
const deck = deckRef.current;
if (!deck) return; // 如果 deck 实例还未创建,则退出
const newLayers = [
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: 12,
getColor: [150, 150, 255],
getAngle: 0,
getTextAnchor: "middle",
getAlignmentBaseline: "center",
getPixelOffset: [0, -10],
visible:
showJunctionTextLayer && currentZoom >= 15 && currentZoom <= 24,
extensions: [new CollisionFilterExtension()],
collisionTestProps: {
sizeScale: 2, // 增加碰撞检测的尺寸以提供更大间距
},
fontSettings: {
sdf: true,
fontSize: 64, // 字体图集大小,默认 64
buffer: 6, // 字符间距缓冲,默认 4
radius: 12, // SDF 半径,默认 12
cutoff: 0.25, // 控制字符粗细,默认 0.25
smoothing: 0.1, // 边缘平滑度,默认 0.1
},
outlineWidth: 10,
outlineColor: [255, 255, 255, 255],
}),
new TextLayer({
id: "pipeTextLayer",
zIndex: 10,
data: showPipeText ? pipeData : [],
getPosition: (d: any) => d.position,
fontFamily: "Monaco, monospace",
getText: (d: any) =>
d[pipeText] ? (d[pipeText] as number).toFixed(3) : "",
getSize: 14,
getColor: [120, 128, 181],
getAngle: (d: any) => d.angle || 0,
getPixelOffset: [0, -8],
getTextAnchor: "middle",
getAlignmentBaseline: "bottom",
visible: showPipeTextLayer && currentZoom >= 15 && currentZoom <= 24,
extensions: [new CollisionFilterExtension()],
collisionTestProps: {
sizeScale: 2, // 增加碰撞检测的尺寸以提供更大间距
},
fontSettings: {
sdf: true,
fontSize: 64, // 字体图集大小,默认 64
buffer: 6, // 字符间距缓冲,默认 4
radius: 12, // SDF 半径,默认 12
cutoff: 0.25, // 控制字符粗细,默认 0.25
smoothing: 0.1, // 边缘平滑度,默认 0.1
},
outlineWidth: 10,
outlineColor: [255, 255, 255, 255],
}),
];
deck.setProps({ layers: newLayers });
// 动画循环
const animate = () => {
if (!deck || !flowAnimation.current) return; // 添加检查,防止空数据或停止旧循环
// 动画总时长(秒)
if (pipeData.length === 0) {
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) => (flowAnimation.current ? d.path : []),
getTimestamps: (d) => {
return d.timestamps; // 这些应该是与 currentTime 匹配的数值
},
getColor: [0, 220, 255],
opacity: 0.8,
visible: currentZoom >= 12 && currentZoom <= 24,
widthMinPixels: 5,
jointRounded: true, // 拐角变圆
// capRounded: true, // 端点变圆
trailLength: 2, // 水流尾迹淡出时间
currentTime: currentTime,
});
// 获取当前除 waterflowLayer 之外的所有图层
const otherLayers = deck.props.layers.filter(
(layer: any) => layer && layer.id !== "waterflowLayer"
);
deck.setProps({
layers: [...otherLayers, waterflowLayer],
});
// 继续请求动画帧,每帧执行一次函数
requestAnimationFrame(animate);
};
animate();
}, [
flowAnimation,
junctionData,
pipeData,
currentZoom,
showJunctionText,
showPipeText,
showJunctionTextLayer,
showPipeTextLayer,
junctionText,
pipeText,
]);
useEffect(() => {
if (pipeText === "flow") {
flowAnimation.current = true;
} else {
flowAnimation.current = false;
}
}, [pipeText]);
// 计算值更新时,更新 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]: record.value,
};
}
return p;
})
);
}, [currentJunctionCalData, currentPipeCalData]);
return (
<>
<DataContext.Provider
value={{
currentJunctionCalData,
setCurrentJunctionCalData,
currentPipeCalData,
setCurrentPipeCalData,
setShowJunctionText,
setShowPipeText,
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;