添加对比模式功能,优化地图组件
This commit is contained in:
@@ -7,6 +7,7 @@ import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { Map as OlMap, VectorTile } from "ol";
|
||||
@@ -49,6 +50,13 @@ interface DataContextType {
|
||||
setCurrentJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
currentPipeCalData?: any[]; // 当前计算结果
|
||||
setCurrentPipeCalData?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
compareJunctionCalData?: any[];
|
||||
setCompareJunctionCalData?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
comparePipeCalData?: any[];
|
||||
setComparePipeCalData?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
isCompareMode?: boolean;
|
||||
setCompareMode?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
toggleCompareMode?: () => void;
|
||||
showJunctionText?: boolean; // 是否显示节点文本
|
||||
showPipeText?: boolean; // 是否显示管道文本
|
||||
showJunctionId?: boolean; // 是否显示节点ID
|
||||
@@ -69,6 +77,10 @@ interface DataContextType {
|
||||
setPipeText?: React.Dispatch<React.SetStateAction<string>>;
|
||||
setContours?: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
deckLayer?: DeckLayer;
|
||||
compareDeckLayer?: DeckLayer;
|
||||
deckLayers?: DeckLayer[];
|
||||
compareMap?: OlMap;
|
||||
maps?: OlMap[];
|
||||
diameterRange?: [number, number];
|
||||
elevationRange?: [number, number];
|
||||
}
|
||||
@@ -128,12 +140,18 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
|
||||
const mapRef = useRef<HTMLDivElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const compareMapRef = useRef<HTMLDivElement | null>(null);
|
||||
const compareCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const deckLayerRef = useRef<DeckLayer | null>(null);
|
||||
const compareDeckLayerRef = useRef<DeckLayer | null>(null);
|
||||
const isDisposingRef = useRef(false);
|
||||
const isCompareDisposingRef = useRef(false);
|
||||
const pendingTimeoutsRef = useRef<number[]>([]);
|
||||
|
||||
const [map, setMap] = useState<OlMap>();
|
||||
const [deckLayer, setDeckLayer] = useState<DeckLayer>();
|
||||
const [compareMap, setCompareMap] = useState<OlMap>();
|
||||
const [compareDeckLayer, setCompareDeckLayer] = useState<DeckLayer>();
|
||||
// currentCalData 用于存储当前计算结果
|
||||
const [currentTime, setCurrentTime] = useState<number>(-1); // 默认选择当前时间
|
||||
// const [selectedDate, setSelectedDate] = useState<Date>(new Date("2025-9-17"));
|
||||
@@ -144,6 +162,11 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
[],
|
||||
);
|
||||
const [currentPipeCalData, setCurrentPipeCalData] = useState<any[]>([]);
|
||||
const [compareJunctionCalData, setCompareJunctionCalData] = useState<any[]>(
|
||||
[],
|
||||
);
|
||||
const [comparePipeCalData, setComparePipeCalData] = useState<any[]>([]);
|
||||
const [isCompareMode, setCompareMode] = useState(false);
|
||||
// junctionData 和 pipeData 分别缓存瓦片解析后节点和管道的数据,用于 deck.gl 定位、标签渲染
|
||||
// currentJunctionCalData 和 currentPipeCalData 变化时会新增并更新 junctionData 和 pipeData 的计算属性值
|
||||
const [junctionData, setJunctionDataState] = useState<any[]>([]);
|
||||
@@ -201,6 +224,37 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
});
|
||||
}, [pipeData, currentPipeCalData, pipeText]);
|
||||
|
||||
const mergedCompareJunctionData = useMemo(() => {
|
||||
const nodeMap = new Map(compareJunctionCalData.map((r: any) => [r.ID, r]));
|
||||
return junctionData.map((j) => {
|
||||
const record = nodeMap.get(j.id);
|
||||
let val = record ? record.value : undefined;
|
||||
if (val !== undefined && junctionText === "actualdemand") {
|
||||
val = toM3h(val, "lps");
|
||||
}
|
||||
return record ? { ...j, [junctionText]: val } : j;
|
||||
});
|
||||
}, [junctionData, compareJunctionCalData, junctionText]);
|
||||
|
||||
const mergedComparePipeData = useMemo(() => {
|
||||
const linkMap = new Map(comparePipeCalData.map((r: any) => [r.ID, r]));
|
||||
return pipeData.map((p) => {
|
||||
const record = linkMap.get(p.id);
|
||||
if (!record) return p;
|
||||
const isFlow = pipeText === "flow";
|
||||
let val = record.value;
|
||||
if (val !== undefined && isFlow) {
|
||||
val = toM3h(val, "lps");
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
[pipeText]: isFlow ? Math.abs(val) : val,
|
||||
flowFlag: isFlow && record.value < 0 ? -1 : 1,
|
||||
path: isFlow && record.value < 0 ? [...p.path].reverse() : p.path,
|
||||
};
|
||||
});
|
||||
}, [pipeData, comparePipeCalData, pipeText]);
|
||||
|
||||
const [diameterRange, setDiameterRange] = useState<
|
||||
[number, number] | undefined
|
||||
>();
|
||||
@@ -208,6 +262,24 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
[number, number] | undefined
|
||||
>();
|
||||
|
||||
const toggleCompareMode = useCallback(() => {
|
||||
setCompareMode((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const maps = useMemo(
|
||||
() =>
|
||||
[map, isCompareMode ? compareMap : undefined].filter(Boolean) as OlMap[],
|
||||
[compareMap, isCompareMode, map],
|
||||
);
|
||||
|
||||
const deckLayers = useMemo(
|
||||
() =>
|
||||
[deckLayer, isCompareMode ? compareDeckLayer : undefined].filter(
|
||||
Boolean,
|
||||
) as DeckLayer[],
|
||||
[compareDeckLayer, deckLayer, isCompareMode],
|
||||
);
|
||||
|
||||
const setJunctionData = (newData: any[]) => {
|
||||
const uniqueNewData = newData.filter((item) => {
|
||||
if (!item || !item.id) return false;
|
||||
@@ -518,6 +590,178 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
},
|
||||
});
|
||||
|
||||
const createOperationalLayers = () => {
|
||||
const nextJunctionSource = new VectorTileSource({
|
||||
url: `${MAP_URL}/gwc/service/tms/1.0.0/${MAP_WORKSPACE}:geo_junctions@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`,
|
||||
format: new MVT(),
|
||||
projection: "EPSG:3857",
|
||||
});
|
||||
const nextPipeSource = new VectorTileSource({
|
||||
url: `${MAP_URL}/gwc/service/tms/1.0.0/${MAP_WORKSPACE}:geo_pipes@WebMercatorQuad@pbf/{z}/{x}/{-y}.pbf`,
|
||||
format: new MVT(),
|
||||
projection: "EPSG:3857",
|
||||
});
|
||||
const nextJunctionsLayer = new WebGLVectorTileLayer({
|
||||
source: nextJunctionSource as any,
|
||||
style: defaultFlatStyle,
|
||||
extent: MAP_EXTENT,
|
||||
maxZoom: 24,
|
||||
minZoom: 11,
|
||||
properties: {
|
||||
name: "节点",
|
||||
value: "junctions",
|
||||
type: "point",
|
||||
properties: [
|
||||
{ name: "高程", value: "elevation" },
|
||||
{ name: "实际需水量", value: "actual_demand" },
|
||||
{ name: "水头", value: "total_head" },
|
||||
{ name: "压力", value: "pressure" },
|
||||
{ name: "水质", value: "quality" },
|
||||
],
|
||||
},
|
||||
});
|
||||
const nextPipesLayer = new WebGLVectorTileLayer({
|
||||
source: nextPipeSource as any,
|
||||
style: defaultFlatStyle,
|
||||
extent: MAP_EXTENT,
|
||||
maxZoom: 24,
|
||||
minZoom: 11,
|
||||
properties: {
|
||||
name: "管道",
|
||||
value: "pipes",
|
||||
type: "linestring",
|
||||
properties: [
|
||||
{ name: "管径", value: "diameter" },
|
||||
{ name: "流量", value: "flow" },
|
||||
{ name: "摩阻系数", value: "friction" },
|
||||
{ name: "水头损失", value: "headloss" },
|
||||
{ name: "单位水头损失", value: "unit_headloss" },
|
||||
{ name: "水质", value: "quality" },
|
||||
{ name: "反应速率", value: "reaction" },
|
||||
{ name: "设置值", value: "setting" },
|
||||
{ name: "状态", value: "status" },
|
||||
{ name: "流速", value: "velocity" },
|
||||
],
|
||||
},
|
||||
});
|
||||
const nextValvesLayer = new WebGLVectorTileLayer({
|
||||
source: valveSource as any,
|
||||
style: valveStyle,
|
||||
extent: MAP_EXTENT,
|
||||
maxZoom: 24,
|
||||
minZoom: 16,
|
||||
properties: {
|
||||
name: "阀门",
|
||||
value: "valves",
|
||||
type: "linestring",
|
||||
properties: [],
|
||||
},
|
||||
});
|
||||
const nextReservoirsLayer = new VectorLayer({
|
||||
source: reservoirSource,
|
||||
style: reservoirStyle,
|
||||
extent: MAP_EXTENT,
|
||||
maxZoom: 24,
|
||||
minZoom: 11,
|
||||
properties: {
|
||||
name: "水库",
|
||||
value: "reservoirs",
|
||||
type: "point",
|
||||
properties: [],
|
||||
},
|
||||
});
|
||||
const nextPumpsLayer = new VectorLayer({
|
||||
source: pumpSource,
|
||||
style: pumpStyle,
|
||||
extent: MAP_EXTENT,
|
||||
maxZoom: 24,
|
||||
minZoom: 11,
|
||||
properties: {
|
||||
name: "水泵",
|
||||
value: "pumps",
|
||||
type: "linestring",
|
||||
properties: [],
|
||||
},
|
||||
});
|
||||
const nextTanksLayer = new VectorLayer({
|
||||
source: tankSource,
|
||||
style: tankStyle,
|
||||
extent: MAP_EXTENT,
|
||||
maxZoom: 24,
|
||||
minZoom: 11,
|
||||
properties: {
|
||||
name: "水箱",
|
||||
value: "tanks",
|
||||
type: "point",
|
||||
properties: [],
|
||||
},
|
||||
});
|
||||
const nextScadaLayer = new VectorLayer({
|
||||
source: scadaSource,
|
||||
style: scadaStyle,
|
||||
extent: MAP_EXTENT,
|
||||
maxZoom: 24,
|
||||
minZoom: 11,
|
||||
properties: {
|
||||
name: "SCADA",
|
||||
value: "scada",
|
||||
type: "point",
|
||||
properties: [],
|
||||
},
|
||||
});
|
||||
|
||||
const availableLayers: any[] = [];
|
||||
config.MAP_AVAILABLE_LAYERS.forEach((layerValue) => {
|
||||
switch (layerValue) {
|
||||
case "junctions":
|
||||
availableLayers.push(nextJunctionsLayer);
|
||||
break;
|
||||
case "pipes":
|
||||
availableLayers.push(nextPipesLayer);
|
||||
break;
|
||||
case "valves":
|
||||
availableLayers.push(nextValvesLayer);
|
||||
break;
|
||||
case "reservoirs":
|
||||
availableLayers.push(nextReservoirsLayer);
|
||||
break;
|
||||
case "pumps":
|
||||
availableLayers.push(nextPumpsLayer);
|
||||
break;
|
||||
case "tanks":
|
||||
availableLayers.push(nextTanksLayer);
|
||||
break;
|
||||
case "scada":
|
||||
availableLayers.push(nextScadaLayer);
|
||||
break;
|
||||
}
|
||||
});
|
||||
availableLayers.sort((a, b) => {
|
||||
const order = [
|
||||
"valves",
|
||||
"junctions",
|
||||
"scada",
|
||||
"reservoirs",
|
||||
"pumps",
|
||||
"tanks",
|
||||
"pipes",
|
||||
].reverse();
|
||||
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);
|
||||
if (ia === -1) ia = order.length;
|
||||
if (ib === -1) ib = order.length;
|
||||
return ia - ib;
|
||||
});
|
||||
|
||||
return availableLayers;
|
||||
};
|
||||
|
||||
// The map and layer instances are intentionally rebuilt only when workspace or extent changes.
|
||||
useEffect(() => {
|
||||
if (!mapRef.current) return;
|
||||
@@ -857,148 +1101,284 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [MAP_WORKSPACE, MAP_EXTENT]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isCompareMode) {
|
||||
isCompareDisposingRef.current = true;
|
||||
setCompareJunctionCalData([]);
|
||||
setComparePipeCalData([]);
|
||||
return;
|
||||
}
|
||||
if (!map || !compareMapRef.current || !compareCanvasRef.current) return;
|
||||
|
||||
isCompareDisposingRef.current = false;
|
||||
const availableLayers = createOperationalLayers();
|
||||
const nextCompareMap = new OlMap({
|
||||
target: compareMapRef.current,
|
||||
view: map.getView(),
|
||||
layers: availableLayers.slice(),
|
||||
controls: [],
|
||||
});
|
||||
nextCompareMap.getAllLayers().forEach((layer) => {
|
||||
const layerId = layer.get("value");
|
||||
if (!layerId) return;
|
||||
const primaryLayer = map
|
||||
.getAllLayers()
|
||||
.find((currentLayer) => currentLayer.get("value") === layerId);
|
||||
if (primaryLayer) {
|
||||
layer.setVisible(primaryLayer.getVisible());
|
||||
}
|
||||
});
|
||||
setCompareMap(nextCompareMap);
|
||||
|
||||
const compareDeck = new Deck({
|
||||
initialViewState: {
|
||||
longitude: 0,
|
||||
latitude: 0,
|
||||
zoom: 1,
|
||||
},
|
||||
canvas: compareCanvasRef.current,
|
||||
controller: false,
|
||||
layers: [],
|
||||
});
|
||||
const nextCompareDeckLayer = new DeckLayer(
|
||||
compareDeck,
|
||||
compareCanvasRef.current,
|
||||
{
|
||||
name: "compareDeckLayer",
|
||||
value: "deckLayer",
|
||||
},
|
||||
);
|
||||
compareDeckLayerRef.current = nextCompareDeckLayer;
|
||||
setCompareDeckLayer(nextCompareDeckLayer);
|
||||
nextCompareMap.addLayer(nextCompareDeckLayer);
|
||||
|
||||
const resizeTimerId = window.setTimeout(() => {
|
||||
map.updateSize();
|
||||
nextCompareMap.updateSize();
|
||||
}, 0);
|
||||
|
||||
return () => {
|
||||
isCompareDisposingRef.current = true;
|
||||
window.clearTimeout(resizeTimerId);
|
||||
if (
|
||||
compareDeckLayerRef.current &&
|
||||
!compareDeckLayerRef.current.isDisposedLayer()
|
||||
) {
|
||||
try {
|
||||
nextCompareMap.removeLayer(compareDeckLayerRef.current);
|
||||
} catch {
|
||||
// Layer may have already been removed during teardown.
|
||||
}
|
||||
compareDeckLayerRef.current.disposeDeck();
|
||||
}
|
||||
compareDeckLayerRef.current = null;
|
||||
setCompareDeckLayer(undefined);
|
||||
setCompareMap(undefined);
|
||||
nextCompareMap.setTarget(undefined);
|
||||
nextCompareMap.dispose();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isCompareMode, map]);
|
||||
|
||||
useEffect(() => {
|
||||
const resizeTimerId = window.setTimeout(() => {
|
||||
map?.updateSize();
|
||||
compareMap?.updateSize();
|
||||
}, 0);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(resizeTimerId);
|
||||
};
|
||||
}, [compareMap, isCompareMode, map]);
|
||||
|
||||
// 当数据变化时,更新 deck.gl 图层
|
||||
useEffect(() => {
|
||||
if (isDisposingRef.current) return;
|
||||
const deckLayer = deckLayerRef.current;
|
||||
if (!deckLayer) return; // 如果 deck 实例还未创建,则退出
|
||||
if (deckLayer.isDisposedLayer()) return;
|
||||
if (!mergedJunctionData.length) return;
|
||||
if (!mergedPipeData.length) return;
|
||||
const junctionTextLayer = new TextLayer({
|
||||
id: "junctionTextLayer",
|
||||
name: "节点文字",
|
||||
zIndex: 10,
|
||||
data: mergedJunctionData,
|
||||
getPosition: (d: any) => d.position,
|
||||
fontFamily: "Monaco, monospace",
|
||||
getText: (d: any) => {
|
||||
let idPart = showJunctionId ? d.id : "";
|
||||
let propPart = "";
|
||||
if (showJunctionTextLayer && d[junctionText] !== undefined) {
|
||||
const value = (d[junctionText] as number).toFixed(3);
|
||||
propPart = `${value}`;
|
||||
}
|
||||
if (idPart && propPart) return `${idPart} - ${propPart}`;
|
||||
return idPart || propPart;
|
||||
},
|
||||
getSize: 14,
|
||||
fontWeight: "bold",
|
||||
getColor: [33, 37, 41], // 深灰色,在灰白背景上清晰可见
|
||||
getAngle: 0,
|
||||
getTextAnchor: "middle",
|
||||
getAlignmentBaseline: "center",
|
||||
getPixelOffset: [0, -10],
|
||||
visible:
|
||||
const syncDeckOverlay = (
|
||||
targetDeckLayer: DeckLayer | null,
|
||||
targetJunctionData: any[],
|
||||
targetPipeData: any[],
|
||||
disposing: boolean,
|
||||
) => {
|
||||
if (disposing || !targetDeckLayer || targetDeckLayer.isDisposedLayer()) {
|
||||
return;
|
||||
}
|
||||
const shouldShowJunctionText =
|
||||
(showJunctionTextLayer || showJunctionId) &&
|
||||
currentZoom >= 15 &&
|
||||
currentZoom <= 24,
|
||||
updateTriggers: {
|
||||
getText: [showJunctionId, showJunctionTextLayer, junctionText],
|
||||
},
|
||||
extensions: [new CollisionFilterExtension()],
|
||||
collisionTestProps: {
|
||||
sizeScale: 3,
|
||||
},
|
||||
characterSet: "auto",
|
||||
fontSettings: {
|
||||
sdf: true,
|
||||
fontSize: 64,
|
||||
buffer: 6,
|
||||
},
|
||||
// outlineWidth: 3,
|
||||
// outlineColor: [255, 255, 255, 220],
|
||||
});
|
||||
|
||||
const pipeTextLayer = new TextLayer({
|
||||
id: "pipeTextLayer",
|
||||
name: "管道文字",
|
||||
zIndex: 10,
|
||||
data: mergedPipeData,
|
||||
getPosition: (d: any) => d.position,
|
||||
fontFamily: "Monaco, monospace",
|
||||
getText: (d: any) => {
|
||||
let idPart = showPipeId ? d.id : "";
|
||||
let propPart = "";
|
||||
if (showPipeTextLayer && d[pipeText] !== undefined) {
|
||||
let value;
|
||||
if (pipeText === "unit_headloss") {
|
||||
value = (
|
||||
(d["unit_headloss"] / (d["length"] / 1000)) as number
|
||||
).toFixed(3);
|
||||
} else {
|
||||
value = Math.abs(d[pipeText] as number).toFixed(3);
|
||||
}
|
||||
propPart = `${value}`;
|
||||
}
|
||||
if (idPart && propPart) return `${idPart} - ${propPart}`;
|
||||
return idPart || propPart;
|
||||
},
|
||||
getSize: 14,
|
||||
fontWeight: "bold",
|
||||
getColor: [33, 37, 41], // 深灰色
|
||||
getAngle: (d: any) => d.angle || 0,
|
||||
getPixelOffset: [0, -8],
|
||||
getTextAnchor: "middle",
|
||||
getAlignmentBaseline: "bottom",
|
||||
visible:
|
||||
currentZoom <= 24 &&
|
||||
targetJunctionData.length > 0;
|
||||
const shouldShowPipeText =
|
||||
(showPipeTextLayer || showPipeId) &&
|
||||
currentZoom >= 15 &&
|
||||
currentZoom <= 24,
|
||||
updateTriggers: {
|
||||
getText: [showPipeId, showPipeTextLayer, pipeText],
|
||||
},
|
||||
extensions: [new CollisionFilterExtension()],
|
||||
collisionTestProps: {
|
||||
sizeScale: 3,
|
||||
},
|
||||
characterSet: "auto",
|
||||
fontSettings: {
|
||||
sdf: true,
|
||||
fontSize: 64,
|
||||
buffer: 6,
|
||||
},
|
||||
// outlineWidth: 3,
|
||||
// outlineColor: [255, 255, 255, 220],
|
||||
});
|
||||
currentZoom <= 24 &&
|
||||
targetPipeData.length > 0;
|
||||
const shouldShowContour =
|
||||
showContourLayer &&
|
||||
currentZoom >= 11 &&
|
||||
currentZoom <= 24 &&
|
||||
targetJunctionData.length > 0;
|
||||
|
||||
const contourLayer = new ContourLayer({
|
||||
id: "junctionContourLayer",
|
||||
name: "等值线",
|
||||
data: mergedJunctionData,
|
||||
aggregation: "MEAN",
|
||||
cellSize: 600,
|
||||
strokeWidth: 0,
|
||||
contours: contours,
|
||||
getPosition: (d) => d.position,
|
||||
getWeight: (d: any) =>
|
||||
(d[junctionText] as number) < 0 ? 0 : (d[junctionText] as number),
|
||||
opacity: 1,
|
||||
visible: showContourLayer && currentZoom >= 11 && currentZoom <= 24,
|
||||
updateTriggers: {
|
||||
// 当 mergedJunctionData 内部数据更新时,通知 getWeight 重新计算
|
||||
getWeight: [mergedJunctionData, junctionText],
|
||||
},
|
||||
});
|
||||
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);
|
||||
}
|
||||
if (deckLayer.getDeckLayerById("junctionContourLayer")) {
|
||||
deckLayer.updateDeckLayer("junctionContourLayer", contourLayer);
|
||||
} else {
|
||||
deckLayer.addDeckLayer(contourLayer);
|
||||
if (!shouldShowJunctionText) {
|
||||
targetDeckLayer.removeDeckLayer("junctionTextLayer");
|
||||
}
|
||||
if (!shouldShowPipeText) {
|
||||
targetDeckLayer.removeDeckLayer("pipeTextLayer");
|
||||
}
|
||||
if (!shouldShowContour) {
|
||||
targetDeckLayer.removeDeckLayer("junctionContourLayer");
|
||||
}
|
||||
if (!shouldShowJunctionText && !shouldShowPipeText && !shouldShowContour) {
|
||||
return;
|
||||
}
|
||||
|
||||
const junctionTextLayer = shouldShowJunctionText
|
||||
? new TextLayer({
|
||||
id: "junctionTextLayer",
|
||||
name: "节点文字",
|
||||
zIndex: 10,
|
||||
data: targetJunctionData,
|
||||
getPosition: (d: any) => d.position,
|
||||
fontFamily: "Monaco, monospace",
|
||||
getText: (d: any) => {
|
||||
let idPart = showJunctionId ? d.id : "";
|
||||
let propPart = "";
|
||||
if (showJunctionTextLayer && d[junctionText] !== undefined) {
|
||||
const value = (d[junctionText] as number).toFixed(3);
|
||||
propPart = `${value}`;
|
||||
}
|
||||
if (idPart && propPart) return `${idPart} - ${propPart}`;
|
||||
return idPart || propPart;
|
||||
},
|
||||
getSize: 14,
|
||||
fontWeight: "bold",
|
||||
getColor: [33, 37, 41],
|
||||
getAngle: 0,
|
||||
getTextAnchor: "middle",
|
||||
getAlignmentBaseline: "center",
|
||||
getPixelOffset: [0, -10],
|
||||
visible: true,
|
||||
updateTriggers: {
|
||||
getText: [showJunctionId, showJunctionTextLayer, junctionText],
|
||||
},
|
||||
extensions: [new CollisionFilterExtension()],
|
||||
collisionTestProps: {
|
||||
sizeScale: 3,
|
||||
},
|
||||
characterSet: "auto",
|
||||
fontSettings: {
|
||||
sdf: true,
|
||||
fontSize: 64,
|
||||
buffer: 6,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
const pipeTextLayer = shouldShowPipeText
|
||||
? new TextLayer({
|
||||
id: "pipeTextLayer",
|
||||
name: "管道文字",
|
||||
zIndex: 10,
|
||||
data: targetPipeData,
|
||||
getPosition: (d: any) => d.position,
|
||||
fontFamily: "Monaco, monospace",
|
||||
getText: (d: any) => {
|
||||
let idPart = showPipeId ? d.id : "";
|
||||
let propPart = "";
|
||||
if (showPipeTextLayer && d[pipeText] !== undefined) {
|
||||
let value;
|
||||
if (pipeText === "unit_headloss") {
|
||||
value = (
|
||||
(d["unit_headloss"] / (d["length"] / 1000)) as number
|
||||
).toFixed(3);
|
||||
} else {
|
||||
value = Math.abs(d[pipeText] as number).toFixed(3);
|
||||
}
|
||||
propPart = `${value}`;
|
||||
}
|
||||
if (idPart && propPart) return `${idPart} - ${propPart}`;
|
||||
return idPart || propPart;
|
||||
},
|
||||
getSize: 14,
|
||||
fontWeight: "bold",
|
||||
getColor: [33, 37, 41],
|
||||
getAngle: (d: any) => d.angle || 0,
|
||||
getPixelOffset: [0, -8],
|
||||
getTextAnchor: "middle",
|
||||
getAlignmentBaseline: "bottom",
|
||||
visible: true,
|
||||
updateTriggers: {
|
||||
getText: [showPipeId, showPipeTextLayer, pipeText],
|
||||
},
|
||||
extensions: [new CollisionFilterExtension()],
|
||||
collisionTestProps: {
|
||||
sizeScale: 3,
|
||||
},
|
||||
characterSet: "auto",
|
||||
fontSettings: {
|
||||
sdf: true,
|
||||
fontSize: 64,
|
||||
buffer: 6,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
const contourLayer = shouldShowContour
|
||||
? new ContourLayer({
|
||||
id: "junctionContourLayer",
|
||||
name: "等值线",
|
||||
data: targetJunctionData,
|
||||
aggregation: "MEAN",
|
||||
cellSize: 600,
|
||||
strokeWidth: 0,
|
||||
contours: contours,
|
||||
getPosition: (d) => d.position,
|
||||
getWeight: (d: any) =>
|
||||
(d[junctionText] as number) < 0 ? 0 : (d[junctionText] as number),
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
updateTriggers: {
|
||||
getWeight: [targetJunctionData, junctionText],
|
||||
},
|
||||
})
|
||||
: null;
|
||||
|
||||
if (junctionTextLayer && targetDeckLayer.getDeckLayerById("junctionTextLayer")) {
|
||||
targetDeckLayer.updateDeckLayer("junctionTextLayer", junctionTextLayer);
|
||||
} else if (junctionTextLayer) {
|
||||
targetDeckLayer.addDeckLayer(junctionTextLayer);
|
||||
}
|
||||
if (pipeTextLayer && targetDeckLayer.getDeckLayerById("pipeTextLayer")) {
|
||||
targetDeckLayer.updateDeckLayer("pipeTextLayer", pipeTextLayer);
|
||||
} else if (pipeTextLayer) {
|
||||
targetDeckLayer.addDeckLayer(pipeTextLayer);
|
||||
}
|
||||
if (contourLayer && targetDeckLayer.getDeckLayerById("junctionContourLayer")) {
|
||||
targetDeckLayer.updateDeckLayer("junctionContourLayer", contourLayer);
|
||||
} else if (contourLayer) {
|
||||
targetDeckLayer.addDeckLayer(contourLayer);
|
||||
}
|
||||
};
|
||||
|
||||
syncDeckOverlay(
|
||||
deckLayerRef.current,
|
||||
mergedJunctionData,
|
||||
mergedPipeData,
|
||||
isDisposingRef.current,
|
||||
);
|
||||
if (isCompareMode) {
|
||||
syncDeckOverlay(
|
||||
compareDeckLayerRef.current,
|
||||
mergedCompareJunctionData,
|
||||
mergedComparePipeData,
|
||||
isCompareDisposingRef.current,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
mergedJunctionData,
|
||||
mergedPipeData,
|
||||
mergedCompareJunctionData,
|
||||
mergedComparePipeData,
|
||||
isCompareMode,
|
||||
junctionText,
|
||||
pipeText,
|
||||
currentZoom,
|
||||
@@ -1012,57 +1392,69 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
|
||||
// 控制流动动画开关
|
||||
useEffect(() => {
|
||||
if (isDisposingRef.current) return;
|
||||
if (pipeText === "flow" && currentPipeCalData.length > 0) {
|
||||
flowAnimation.current = true;
|
||||
} else {
|
||||
flowAnimation.current = false;
|
||||
}
|
||||
const deckLayer = deckLayerRef.current;
|
||||
if (!deckLayer) return; // 如果 deck 实例还未创建,则退出
|
||||
flowAnimation.current = pipeText === "flow" && currentPipeCalData.length > 0;
|
||||
const shouldShowWaterflow =
|
||||
isWaterflowLayerAvailable &&
|
||||
showWaterflowLayer &&
|
||||
flowAnimation.current &&
|
||||
currentZoom >= 12 &&
|
||||
currentZoom <= 24;
|
||||
|
||||
let animationFrameId: number; // 保存 requestAnimationFrame 的 ID
|
||||
let animationFrameId: number;
|
||||
|
||||
// 动画循环
|
||||
const animate = () => {
|
||||
if (isDisposingRef.current || deckLayer.isDisposedLayer()) return;
|
||||
// 动画总时长(秒)
|
||||
const syncWaterflowLayer = (
|
||||
targetDeckLayer: DeckLayer | null,
|
||||
targetPipeData: any[],
|
||||
disposing: boolean,
|
||||
) => {
|
||||
if (disposing || !targetDeckLayer || targetDeckLayer.isDisposedLayer()) {
|
||||
return;
|
||||
}
|
||||
if (!shouldShowWaterflow || targetPipeData.length === 0) {
|
||||
targetDeckLayer.removeDeckLayer("waterflowLayer");
|
||||
return;
|
||||
}
|
||||
const animationDuration = 10;
|
||||
const bufferTime = 2;
|
||||
const loopLength = animationDuration + bufferTime;
|
||||
const currentTime = (Date.now() / 1000) % loopLength;
|
||||
const currentFrameTime = (Date.now() / 1000) % loopLength;
|
||||
|
||||
const waterflowLayer = new TripsLayer({
|
||||
id: "waterflowLayer",
|
||||
name: "水流",
|
||||
data: mergedPipeData,
|
||||
data: targetPipeData,
|
||||
getPath: (d) => d.path,
|
||||
getTimestamps: (d) => {
|
||||
return d.timestamps; // 这些应该是与 currentTime 匹配的数值
|
||||
},
|
||||
getTimestamps: (d) => d.timestamps,
|
||||
getColor: [0, 220, 255],
|
||||
opacity: 0.8,
|
||||
visible:
|
||||
isWaterflowLayerAvailable &&
|
||||
showWaterflowLayer &&
|
||||
flowAnimation.current && // 保持动画标志作为可见性的一部分
|
||||
currentZoom >= 12 &&
|
||||
currentZoom <= 24,
|
||||
visible: true,
|
||||
widthMinPixels: 5,
|
||||
jointRounded: true, // 拐角变圆
|
||||
// capRounded: true, // 端点变圆
|
||||
trailLength: 2, // 水流尾迹淡出时间
|
||||
currentTime: currentTime,
|
||||
jointRounded: true,
|
||||
trailLength: 2,
|
||||
currentTime: currentFrameTime,
|
||||
});
|
||||
|
||||
if (deckLayer.getDeckLayerById("waterflowLayer")) {
|
||||
deckLayer.updateDeckLayer("waterflowLayer", waterflowLayer);
|
||||
if (targetDeckLayer.getDeckLayerById("waterflowLayer")) {
|
||||
targetDeckLayer.updateDeckLayer("waterflowLayer", waterflowLayer);
|
||||
} else {
|
||||
deckLayer.addDeckLayer(waterflowLayer);
|
||||
targetDeckLayer.addDeckLayer(waterflowLayer);
|
||||
}
|
||||
};
|
||||
|
||||
// 只有在需要动画时才请求下一帧,但图层已经添加到了 deckLayer 中
|
||||
if (flowAnimation.current) {
|
||||
const animate = () => {
|
||||
syncWaterflowLayer(
|
||||
deckLayerRef.current,
|
||||
mergedPipeData,
|
||||
isDisposingRef.current,
|
||||
);
|
||||
if (isCompareMode) {
|
||||
syncWaterflowLayer(
|
||||
compareDeckLayerRef.current,
|
||||
mergedComparePipeData,
|
||||
isCompareDisposingRef.current,
|
||||
);
|
||||
}
|
||||
if (shouldShowWaterflow) {
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
@@ -1078,6 +1470,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
currentPipeCalData,
|
||||
currentZoom,
|
||||
mergedPipeData,
|
||||
mergedComparePipeData,
|
||||
isCompareMode,
|
||||
pipeText,
|
||||
isWaterflowLayerAvailable,
|
||||
showWaterflowLayer,
|
||||
@@ -1097,6 +1491,13 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
setCurrentJunctionCalData,
|
||||
currentPipeCalData,
|
||||
setCurrentPipeCalData,
|
||||
compareJunctionCalData,
|
||||
setCompareJunctionCalData,
|
||||
comparePipeCalData,
|
||||
setComparePipeCalData,
|
||||
isCompareMode,
|
||||
setCompareMode,
|
||||
toggleCompareMode,
|
||||
setShowJunctionTextLayer,
|
||||
setShowPipeTextLayer,
|
||||
setShowJunctionId,
|
||||
@@ -1115,17 +1516,50 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
pipeText,
|
||||
setContours,
|
||||
deckLayer,
|
||||
compareDeckLayer,
|
||||
deckLayers,
|
||||
compareMap,
|
||||
maps,
|
||||
diameterRange,
|
||||
elevationRange,
|
||||
}}
|
||||
>
|
||||
<MapContext.Provider value={map}>
|
||||
<div className="relative w-full h-full">
|
||||
<div ref={mapRef} className="w-full h-full"></div>
|
||||
<div className="flex w-full h-full">
|
||||
<div
|
||||
className={`relative h-full ${isCompareMode ? "w-1/2" : "w-full"}`}
|
||||
>
|
||||
<div ref={mapRef} className="w-full h-full"></div>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="pointer-events-none absolute inset-0"
|
||||
/>
|
||||
{isCompareMode && (
|
||||
<div className="pointer-events-none absolute left-4 top-4 rounded-md bg-black/55 px-3 py-1 text-sm font-medium text-white">
|
||||
方案模拟
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCompareMode && (
|
||||
<div className="relative h-full w-1/2 border-l border-white/40">
|
||||
<div ref={compareMapRef} className="w-full h-full"></div>
|
||||
<canvas
|
||||
ref={compareCanvasRef}
|
||||
className="pointer-events-none absolute inset-0"
|
||||
/>
|
||||
<div className="pointer-events-none absolute left-4 top-4 rounded-md bg-black/55 px-3 py-1 text-sm font-medium text-white">
|
||||
实时模拟
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isCompareMode && (
|
||||
<div className="pointer-events-none absolute inset-y-0 left-1/2 z-10 w-px -translate-x-1/2 bg-white/85 shadow-[0_0_0_1px_rgba(15,23,42,0.18)]" />
|
||||
)}
|
||||
<MapTools />
|
||||
{children}
|
||||
</div>
|
||||
<canvas ref={canvasRef} />
|
||||
</MapContext.Provider>
|
||||
</DataContext.Provider>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user