548 lines
18 KiB
TypeScript
548 lines
18 KiB
TypeScript
// @refresh reset // 添加此注释强制热重载时重新挂载组件
|
||
"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";
|
||
|
||
// 创建自定义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 extent = config.mapExtent;
|
||
const backendUrl = config.backendUrl;
|
||
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);
|
||
};
|
||
|
||
const MapComponent: React.FC = () => {
|
||
const mapRef = useRef<HTMLDivElement | null>(null);
|
||
const deckRef = useRef<Deck | null>(null);
|
||
|
||
const [map, setMap] = useState<OlMap>();
|
||
const [currentTime, setCurrentTime] = useState(
|
||
new Date("2025-09-17T00:30:00+08:00")
|
||
);
|
||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||
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[]>([]);
|
||
|
||
let showJunctionText = true; // 控制节点文本显示
|
||
let showPipeText = true; // 控制管道文本显示
|
||
let junctionText = "pressure";
|
||
let pipeText = "flow";
|
||
const isAnimating = useRef(false); // 添加动画控制标志
|
||
|
||
// 防抖更新函数
|
||
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 setFrameData = async (queryTime: Date) => {
|
||
const query_time = queryTime.toISOString();
|
||
console.log("Query Time:", query_time);
|
||
try {
|
||
// 定义需要查询的属性
|
||
const junctionProperties = junctionText;
|
||
const pipeProperties = pipeText;
|
||
// 同时查询节点和管道数据
|
||
const starttime = Date.now();
|
||
const [nodeResponse, linkResponse] = await Promise.all([
|
||
fetch(
|
||
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=node&property=${junctionProperties}`
|
||
),
|
||
fetch(
|
||
`${backendUrl}/queryallrecordsbytimeproperty/?querytime=${query_time}&type=link&property=${pipeProperties}`
|
||
),
|
||
]);
|
||
|
||
const nodeRecords = await nodeResponse.json();
|
||
const linkRecords = await linkResponse.json();
|
||
// 将 nodeRecords 转换为 Map 以提高查找效率
|
||
const nodeMap: Map<string, any> = new Map(
|
||
nodeRecords.results.map((r: any) => [r.ID, r])
|
||
);
|
||
// 将 linkRecords 转换为 Map 以提高查找效率
|
||
const linkMap: Map<string, any> = new Map(
|
||
linkRecords.results.map((r: any) => [r.ID, r])
|
||
);
|
||
|
||
// 更新junctionData
|
||
setJunctionDataState((prev) =>
|
||
prev.map((j) => {
|
||
const record = nodeMap.get(j.id);
|
||
if (record) {
|
||
return {
|
||
...j,
|
||
[junctionProperties]: record.value,
|
||
};
|
||
}
|
||
return j;
|
||
})
|
||
);
|
||
|
||
// 更新pipeData
|
||
setPipeDataState((prev) =>
|
||
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;
|
||
})
|
||
);
|
||
// 属性为 flow 时启动动画
|
||
if (pipeProperties === "flow") {
|
||
isAnimating.current = true;
|
||
} else {
|
||
isAnimating.current = false;
|
||
}
|
||
const endtime = Date.now();
|
||
console.log("Data fetch and update time:", endtime - starttime, "ms");
|
||
} catch (error) {
|
||
console.error("Error fetching data:", error);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (!mapRef.current) return;
|
||
// 添加 MVT 瓦片加载逻辑
|
||
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",
|
||
});
|
||
|
||
// 缓存数据
|
||
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);
|
||
}
|
||
});
|
||
|
||
// 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" },
|
||
],
|
||
},
|
||
});
|
||
|
||
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" },
|
||
],
|
||
},
|
||
});
|
||
|
||
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, // 动画持续时间
|
||
});
|
||
// 初始化 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 () => {
|
||
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: 1000,
|
||
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],
|
||
// --- 修改以下属性 ---
|
||
// characterSet: "auto",
|
||
// outlineWidth: 4,
|
||
// outlineColor: [255, 255, 255, 255], // 设置为白色轮廓
|
||
}),
|
||
new TextLayer({
|
||
id: "pipeTextLayer",
|
||
zIndex: 1000,
|
||
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",
|
||
// --- 修改以下属性 ---
|
||
// characterSet: "auto",
|
||
// outlineWidth: 5,
|
||
// outlineColor: [255, 255, 255, 255], // 设置为白色轮廓
|
||
}),
|
||
];
|
||
deck.setProps({ layers: newLayers });
|
||
|
||
// 动画循环
|
||
const animate = () => {
|
||
if (!deck || !isAnimating.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) => (isAnimating.current ? d.path : []),
|
||
getTimestamps: (d) => {
|
||
return d.timestamps; // 这些应该是与 currentTime 匹配的数值
|
||
},
|
||
getColor: [0, 220, 255],
|
||
opacity: 0.8,
|
||
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();
|
||
}, [isAnimating, junctionData, pipeData]);
|
||
|
||
// 启动时间更新interval
|
||
useEffect(() => {
|
||
intervalRef.current = setInterval(() => {
|
||
setCurrentTime((prev) => new Date(prev.getTime() + 1800 * 1000));
|
||
}, 10 * 1000);
|
||
return () => {
|
||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||
};
|
||
}, []);
|
||
|
||
// 当currentTime改变时,获取数据
|
||
useEffect(() => {
|
||
const fetchData = async () => {
|
||
await setFrameData(currentTime);
|
||
};
|
||
fetchData();
|
||
}, [currentTime]);
|
||
|
||
return (
|
||
<>
|
||
<MapContext.Provider value={map}>
|
||
<div className="relative w-full h-full">
|
||
<div ref={mapRef} className="w-full h-full"></div>
|
||
<MapTools />
|
||
</div>
|
||
<canvas id="deck-canvas" />
|
||
</MapContext.Provider>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default MapComponent;
|