完成页面的基础配置
This commit is contained in:
547
src/app/OlMap/MapComponent.tsx
Normal file
547
src/app/OlMap/MapComponent.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user