完成页面的基础配置

This commit is contained in:
JIANG
2025-09-28 15:51:45 +08:00
parent e34dc99330
commit 6d1cc6c9a1
28 changed files with 9753 additions and 52 deletions

View File

@@ -0,0 +1,547 @@
// @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;