完成时间轴前后端数据连通
This commit is contained in:
@@ -74,10 +74,11 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full overflow-hidden">
|
<div className="relative w-full h-full overflow-hidden">
|
||||||
<MapComponent />
|
<MapComponent>
|
||||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 w-[800px] opacity-90 hover:opacity-100 transition-opacity duration-300">
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-10 w-[800px] opacity-90 hover:opacity-100 transition-opacity duration-300">
|
||||||
<Timeline />
|
<Timeline />
|
||||||
</div>
|
</div>
|
||||||
|
</MapComponent>
|
||||||
<SCADADeviceList
|
<SCADADeviceList
|
||||||
devices={devices}
|
devices={devices}
|
||||||
onDeviceClick={handleDeviceClick}
|
onDeviceClick={handleDeviceClick}
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ const DrawPanel: React.FC = () => {
|
|||||||
const isSaveDisabled = drawnFeatures.length === 0;
|
const isSaveDisabled = drawnFeatures.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-20 left-4 bg-white p-1 rounded-xl shadow-lg flex flex-col opacity-85 hover:opacity-100 transition-opacity">
|
<div className="absolute top-20 left-4 bg-white p-1 rounded-xl shadow-lg flex flex-col opacity-85 hover:opacity-100 transition-opacity z-10">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<BackHandOutlinedIcon />}
|
icon={<BackHandOutlinedIcon />}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-0 right-0 h-auto bg-white p-6 rounded-bl-2xl shadow-lg min-w-[320px]">
|
<div className="absolute top-0 right-0 h-auto bg-white p-6 rounded-bl-2xl shadow-lg min-w-[320px] z-10">
|
||||||
<h3 className="text-lg font-semibold mb-4">属性面板</h3>
|
<h3 className="text-lg font-semibold mb-4">属性面板</h3>
|
||||||
<p className="text-gray-500">请选择一个要素以查看其属性。</p>
|
<p className="text-gray-500">请选择一个要素以查看其属性。</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +38,7 @@ const PropertyPanel: React.FC<PropertyPanelProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-0 right-0 h-auto bg-white p-6 rounded-bl-2xl shadow-lg min-w-[320px]">
|
<div className="absolute top-0 right-0 h-auto bg-white p-6 rounded-bl-2xl shadow-lg min-w-[320px] z-10">
|
||||||
<h3 className="text-lg font-semibold mb-4">属性面板</h3>
|
<h3 className="text-lg font-semibold mb-4">属性面板</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ const Scale: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-0 right-0 flex col-auto px-2 bg-white bg-opacity-70 text-black rounded-tl shadow-md text-sm">
|
<div className="absolute bottom-0 right-0 flex col-auto px-2 bg-white bg-opacity-70 text-black rounded-tl shadow-md text-sm">
|
||||||
<div className="px-1">Zoom Level: {zoomLevel.toFixed(1)}</div>
|
<div className="px-1">缩放: {zoomLevel.toFixed(1)}</div>
|
||||||
<div className="px-1">
|
<div className="px-1">
|
||||||
Coordinates: {coordinates[0]}, {coordinates[1]}
|
坐标: {coordinates[0]}, {coordinates[1]}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,11 +19,14 @@ import {
|
|||||||
|
|
||||||
// 导入OpenLayers样式相关模块
|
// 导入OpenLayers样式相关模块
|
||||||
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
||||||
import { useMap } from "../MapComponent";
|
import { useData, useMap } from "../MapComponent";
|
||||||
|
|
||||||
import StyleLegend, { LegendStyleConfig } from "./StyleLegend";
|
import StyleLegend, { LegendStyleConfig } from "./StyleLegend";
|
||||||
import { FlatStyleLike } from "ol/style/flat";
|
import { FlatStyleLike } from "ol/style/flat";
|
||||||
|
|
||||||
|
import { calculateClassification } from "@utils/breaks_classification";
|
||||||
|
import { parseColor } from "@utils/parseColor";
|
||||||
|
|
||||||
interface StyleConfig {
|
interface StyleConfig {
|
||||||
property: string;
|
property: string;
|
||||||
classificationMethod: string; // 分类方法
|
classificationMethod: string; // 分类方法
|
||||||
@@ -96,6 +99,22 @@ const CLASSIFICATION_METHODS = [
|
|||||||
|
|
||||||
const StyleEditorPanel: React.FC = () => {
|
const StyleEditorPanel: React.FC = () => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
|
const data = useData();
|
||||||
|
if (!data) {
|
||||||
|
return <div>Loading...</div>; // 或其他占位符
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
junctionData,
|
||||||
|
pipeData,
|
||||||
|
setShowJunctionText,
|
||||||
|
setShowPipeText,
|
||||||
|
setJunctionText,
|
||||||
|
setPipeText,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
const [applyJunctionStyle, setApplyJunctionStyle] = useState(false);
|
||||||
|
const [applyPipeStyle, setApplyPipeStyle] = useState(false);
|
||||||
|
|
||||||
const [renderLayers, setRenderLayers] = useState<WebGLVectorTileLayer[]>([]);
|
const [renderLayers, setRenderLayers] = useState<WebGLVectorTileLayer[]>([]);
|
||||||
const [selectedRenderLayer, setSelectedRenderLayer] =
|
const [selectedRenderLayer, setSelectedRenderLayer] =
|
||||||
useState<WebGLVectorTileLayer>();
|
useState<WebGLVectorTileLayer>();
|
||||||
@@ -134,75 +153,10 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
breaks: [],
|
breaks: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 样式状态管理 - 存储多个图层的样式状态
|
// 样式状态管理 - 存储多个图层的样式状态
|
||||||
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>(
|
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 通用颜色解析函数
|
|
||||||
const parseColor = useCallback((color: string) => {
|
|
||||||
// 解析 rgba 格式的颜色
|
|
||||||
const match = color.match(
|
|
||||||
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
|
|
||||||
);
|
|
||||||
if (match) {
|
|
||||||
return {
|
|
||||||
r: parseInt(match[1], 10),
|
|
||||||
g: parseInt(match[2], 10),
|
|
||||||
b: parseInt(match[3], 10),
|
|
||||||
// 如果没有 alpha 值,默认为 1
|
|
||||||
a: match[4] ? parseFloat(match[4]) : 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 如果还是十六进制格式,保持原来的解析方式
|
|
||||||
const hex = color.replace("#", "");
|
|
||||||
return {
|
|
||||||
r: parseInt(hex.slice(0, 2), 16),
|
|
||||||
g: parseInt(hex.slice(2, 4), 16),
|
|
||||||
b: parseInt(hex.slice(4, 6), 16),
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
// 获取数据分段分类结果
|
|
||||||
const fetchClassification = async (
|
|
||||||
layer_name: string,
|
|
||||||
prop: string,
|
|
||||||
n_classes: number,
|
|
||||||
algorithm: string
|
|
||||||
) => {
|
|
||||||
if (!algorithm) {
|
|
||||||
algorithm = "pretty_breaks"; // 默认算法
|
|
||||||
}
|
|
||||||
const response = await fetch("http://localhost:8000/jenks-classification", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
layer_name: layer_name, // 图层名称
|
|
||||||
prop: prop, // 属性名称
|
|
||||||
n_classes: n_classes, // 分段数
|
|
||||||
algorithm: algorithm,
|
|
||||||
// algorithm: "pretty_breaks",
|
|
||||||
// algorithm: "hybrid_jenks"
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("API 请求失败:", response.status, response.statusText);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const breaks = data.breaks; // 从响应对象中提取 breaks 数组
|
|
||||||
// console.log(breaks);
|
|
||||||
// 验证返回的数据
|
|
||||||
if (!Array.isArray(breaks) || breaks.length === 0) {
|
|
||||||
console.error("API 返回的 breaks 不是有效数组:", breaks);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return breaks;
|
|
||||||
};
|
|
||||||
// 颜色方案选择
|
// 颜色方案选择
|
||||||
const [singlePaletteIndex, setSinglePaletteIndex] = useState(0);
|
const [singlePaletteIndex, setSinglePaletteIndex] = useState(0);
|
||||||
const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0);
|
const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0);
|
||||||
@@ -232,21 +186,45 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
[gradientPaletteIndex, parseColor]
|
[gradientPaletteIndex, parseColor]
|
||||||
);
|
);
|
||||||
// 应用分类样式
|
// 应用分类样式
|
||||||
const applyStyle = (breaks?: number[]) => {
|
const setStyleState = (layer: any) => {
|
||||||
|
if (
|
||||||
|
layer.get("value") !== undefined &&
|
||||||
|
styleConfig.property !== undefined
|
||||||
|
) {
|
||||||
|
// 更新文字标签设置
|
||||||
|
if (layer.get("value") === "junctions") {
|
||||||
|
if (setJunctionText && setShowJunctionText) {
|
||||||
|
setJunctionText(styleConfig.property);
|
||||||
|
setShowJunctionText(styleConfig.showLabels);
|
||||||
|
setApplyJunctionStyle(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (layer.get("value") === "pipes") {
|
||||||
|
if (setPipeText && setShowPipeText) {
|
||||||
|
setPipeText(styleConfig.property);
|
||||||
|
setShowPipeText(styleConfig.showLabels);
|
||||||
|
setApplyPipeStyle(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const applyStyle = (layerId: string, breaks?: number[]) => {
|
||||||
// 使用传入的 breaks 数据
|
// 使用传入的 breaks 数据
|
||||||
if (!breaks) {
|
if (!breaks || breaks.length === 0) {
|
||||||
console.warn("没有有效的 breaks 数据");
|
console.warn("没有有效的 breaks 数据");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!selectedRenderLayer || !styleConfig.property) return;
|
const styleConfig = layerStyleStates.find(
|
||||||
|
(s) => s.layerId === layerId
|
||||||
|
)?.styleConfig;
|
||||||
|
const selectedRenderLayer = renderLayers.find(
|
||||||
|
(l) => l.get("id") === layerId
|
||||||
|
);
|
||||||
|
if (!selectedRenderLayer || !styleConfig?.property) return;
|
||||||
const layerType: string = selectedRenderLayer?.get("type");
|
const layerType: string = selectedRenderLayer?.get("type");
|
||||||
const source = selectedRenderLayer.getSource();
|
const source = selectedRenderLayer.getSource();
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
|
|
||||||
if (breaks.length === 0) {
|
|
||||||
console.log("没有有效的 breaks 数据,无法应用样式");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const breaksLength = breaks.length;
|
const breaksLength = breaks.length;
|
||||||
// 根据 breaks 计算每个分段的颜色,线条粗细
|
// 根据 breaks 计算每个分段的颜色,线条粗细
|
||||||
const colors: string[] =
|
const colors: string[] =
|
||||||
@@ -385,8 +363,45 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
setLayerStyleStates((prev) =>
|
setLayerStyleStates((prev) =>
|
||||||
prev.filter((state) => state.layerId !== layerId)
|
prev.filter((state) => state.layerId !== layerId)
|
||||||
);
|
);
|
||||||
|
// 重置样式应用状态
|
||||||
|
if (layerId === "junctions") {
|
||||||
|
setApplyJunctionStyle(false);
|
||||||
|
} else if (layerId === "pipes") {
|
||||||
|
setApplyPipeStyle(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [selectedRenderLayer]);
|
}, [selectedRenderLayer]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (applyJunctionStyle && junctionData.length > 0) {
|
||||||
|
// 应用节点样式
|
||||||
|
const junctionStyleConfigState = layerStyleStates.find(
|
||||||
|
(s) => s.layerId === "junctions"
|
||||||
|
);
|
||||||
|
if (!junctionStyleConfigState) return;
|
||||||
|
const segments = junctionStyleConfigState?.styleConfig.segments;
|
||||||
|
const breaks = calculateClassification(
|
||||||
|
junctionData,
|
||||||
|
segments,
|
||||||
|
styleConfig.classificationMethod
|
||||||
|
);
|
||||||
|
applyStyle(junctionStyleConfigState.layerId, breaks);
|
||||||
|
}
|
||||||
|
if (applyPipeStyle && pipeData.length > 0) {
|
||||||
|
// 应用管道样式
|
||||||
|
const pipeStyleConfigState = layerStyleStates.find(
|
||||||
|
(s) => s.layerId === "pipes"
|
||||||
|
);
|
||||||
|
if (!pipeStyleConfigState) return;
|
||||||
|
const segments = pipeStyleConfigState?.styleConfig.segments;
|
||||||
|
const breaks = calculateClassification(
|
||||||
|
pipeData,
|
||||||
|
segments,
|
||||||
|
styleConfig.classificationMethod
|
||||||
|
);
|
||||||
|
applyStyle(pipeStyleConfigState.layerId, breaks);
|
||||||
|
}
|
||||||
|
}, [junctionData, pipeData, applyJunctionStyle, applyPipeStyle]);
|
||||||
// 样式状态管理功能
|
// 样式状态管理功能
|
||||||
// 保存当前图层的样式状态
|
// 保存当前图层的样式状态
|
||||||
const saveLayerStyle = useCallback(
|
const saveLayerStyle = useCallback(
|
||||||
@@ -436,8 +451,9 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
const updateVisibleLayers = () => {
|
const updateVisibleLayers = () => {
|
||||||
const layers = map.getAllLayers();
|
const layers = map.getAllLayers();
|
||||||
// 筛选矢量瓦片图层
|
// 筛选矢量瓦片图层
|
||||||
const webGLVectorTileLayers = layers.filter((layer) =>
|
const webGLVectorTileLayers = layers.filter(
|
||||||
layer.get("value")
|
(layer) =>
|
||||||
|
layer.get("value") === "junctions" || layer.get("value") === "pipes" // 暂时只处理这两个图层
|
||||||
) as WebGLVectorTileLayer[];
|
) as WebGLVectorTileLayer[];
|
||||||
|
|
||||||
setRenderLayers(webGLVectorTileLayers);
|
setRenderLayers(webGLVectorTileLayers);
|
||||||
@@ -505,6 +521,7 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [styleConfig.colorType]);
|
}, [styleConfig.colorType]);
|
||||||
|
|
||||||
// 获取所有激活的图例配置
|
// 获取所有激活的图例配置
|
||||||
const getActiveLegendConfigs = useCallback(() => {
|
const getActiveLegendConfigs = useCallback(() => {
|
||||||
return layerStyleStates
|
return layerStyleStates
|
||||||
@@ -803,7 +820,7 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="absolute top-20 left-4 bg-white p-4 rounded-xl shadow-lg opacity-95 hover:opacity-100 transition-opacity w-80">
|
<div className="absolute top-20 left-4 bg-white p-4 rounded-xl shadow-lg opacity-95 hover:opacity-100 transition-opacity w-80 z-10">
|
||||||
{/* 图层选择 */}
|
{/* 图层选择 */}
|
||||||
<FormControl variant="standard" fullWidth margin="dense">
|
<FormControl variant="standard" fullWidth margin="dense">
|
||||||
<InputLabel>选择图层</InputLabel>
|
<InputLabel>选择图层</InputLabel>
|
||||||
@@ -948,25 +965,8 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
// 获取分段数据后应用样式
|
setStyleState(selectedRenderLayer);
|
||||||
if (
|
|
||||||
selectedRenderLayer &&
|
|
||||||
selectedRenderLayer.get("value") !== undefined &&
|
|
||||||
styleConfig.property !== undefined &&
|
|
||||||
styleConfig.segments !== undefined
|
|
||||||
) {
|
|
||||||
const newBreaks = await fetchClassification(
|
|
||||||
selectedRenderLayer?.get("value"),
|
|
||||||
styleConfig.property,
|
|
||||||
styleConfig.segments,
|
|
||||||
styleConfig.classificationMethod
|
|
||||||
);
|
|
||||||
if (newBreaks) {
|
|
||||||
applyStyle(newBreaks);
|
|
||||||
// setShowLegend(true); // 应用样式后显示图例
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={!selectedRenderLayer || !styleConfig.property}
|
disabled={!selectedRenderLayer || !styleConfig.property}
|
||||||
startIcon={<ApplyIcon />}
|
startIcon={<ApplyIcon />}
|
||||||
@@ -989,7 +989,7 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
{/* 显示多图层图例 */}
|
{/* 显示多图层图例 */}
|
||||||
{getActiveLegendConfigs().length > 0 && (
|
{getActiveLegendConfigs().length > 0 && (
|
||||||
<div className="fixed bottom-35 right-4 flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
|
<div className=" absolute bottom-40 right-4 shadow-lg flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
|
||||||
<div className="flex flex-row gap-3">
|
<div className="flex flex-row gap-3">
|
||||||
{getActiveLegendConfigs().map((config, index) => (
|
{getActiveLegendConfigs().map((config, index) => (
|
||||||
<StyleLegend key={`${config.layerId}-${index}`} {...config} />
|
<StyleLegend key={`${config.layerId}-${index}`} {...config} />
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const StyleLegend: React.FC<LegendStyleConfig> = ({
|
|||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
key={layerId}
|
key={layerId}
|
||||||
className="bg-white p-3 rounded-xl shadow-lg max-w-xs opacity-95"
|
className="bg-white p-3 rounded-xl shadow-lg max-w-xs opacity-95 transition-opacity duration-300 hover:opacity-100"
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
<Typography variant="subtitle2" gutterBottom>
|
||||||
{layerName} - {property}
|
{layerName} - {property}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
Slider,
|
Slider,
|
||||||
Typography,
|
Typography,
|
||||||
Paper,
|
Paper,
|
||||||
TextField,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Select,
|
Select,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -22,26 +21,18 @@ import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
|
|||||||
import { zhCN } from "date-fns/locale";
|
import { zhCN } from "date-fns/locale";
|
||||||
import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material";
|
import { PlayArrow, Pause, Stop, Refresh } from "@mui/icons-material";
|
||||||
import { TbRewindBackward5, TbRewindForward5 } from "react-icons/tb";
|
import { TbRewindBackward5, TbRewindForward5 } from "react-icons/tb";
|
||||||
|
import { useData } from "../MapComponent";
|
||||||
|
import { config } from "@/config/config";
|
||||||
|
|
||||||
interface TimelineProps {
|
const backendUrl = config.backendUrl;
|
||||||
onTimeChange?: (time: string) => void;
|
const Timeline: React.FC = () => {
|
||||||
onDateChange?: (date: Date) => void;
|
const data = useData();
|
||||||
onPlay?: () => void;
|
if (!data) {
|
||||||
onPause?: () => void;
|
return <div>Loading...</div>; // 或其他占位符
|
||||||
onStop?: () => void;
|
}
|
||||||
onRefresh?: () => void;
|
const { setJunctionDataState, setPipeDataState, junctionText, pipeText } =
|
||||||
onFetch?: () => void;
|
data;
|
||||||
}
|
|
||||||
|
|
||||||
const Timeline: React.FC<TimelineProps> = ({
|
|
||||||
onTimeChange,
|
|
||||||
onDateChange,
|
|
||||||
onPlay,
|
|
||||||
onPause,
|
|
||||||
onStop,
|
|
||||||
onRefresh,
|
|
||||||
onFetch,
|
|
||||||
}) => {
|
|
||||||
const [currentTime, setCurrentTime] = useState<number>(0); // 分钟数 (0-1439)
|
const [currentTime, setCurrentTime] = useState<number>(0); // 分钟数 (0-1439)
|
||||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
@@ -51,6 +42,114 @@ const Timeline: React.FC<TimelineProps> = ({
|
|||||||
|
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const timelineRef = useRef<HTMLDivElement>(null);
|
const timelineRef = useRef<HTMLDivElement>(null);
|
||||||
|
// 添加缓存引用
|
||||||
|
const cacheRef = useRef<
|
||||||
|
Map<string, { nodeRecords: any[]; linkRecords: any[] }>
|
||||||
|
>(new Map());
|
||||||
|
// 添加防抖引用
|
||||||
|
const debounceRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const fetchFrameData = async (queryTime: Date) => {
|
||||||
|
const query_time = queryTime.toISOString();
|
||||||
|
const cacheKey = query_time;
|
||||||
|
|
||||||
|
// 检查缓存
|
||||||
|
if (cacheRef.current.has(cacheKey)) {
|
||||||
|
const { nodeRecords, linkRecords } = cacheRef.current.get(cacheKey)!;
|
||||||
|
// 使用缓存数据更新状态
|
||||||
|
updateDataStates(nodeRecords, linkRecords);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 定义需要查询的属性
|
||||||
|
const junctionProperties = junctionText;
|
||||||
|
const pipeProperties = pipeText;
|
||||||
|
if (
|
||||||
|
!junctionProperties ||
|
||||||
|
!pipeProperties ||
|
||||||
|
junctionProperties === "" ||
|
||||||
|
pipeProperties === ""
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"Query Time:",
|
||||||
|
queryTime.toLocaleDateString() + " " + queryTime.toLocaleTimeString()
|
||||||
|
);
|
||||||
|
// 同时查询节点和管道数据
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 缓存数据
|
||||||
|
cacheRef.current.set(cacheKey, {
|
||||||
|
nodeRecords: nodeRecords.results,
|
||||||
|
linkRecords: linkRecords.results,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
updateDataStates(nodeRecords.results, linkRecords.results);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching data:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提取更新状态的逻辑
|
||||||
|
const updateDataStates = (nodeResults: any[], linkResults: any[]) => {
|
||||||
|
const junctionProperties = junctionText;
|
||||||
|
const pipeProperties = pipeText;
|
||||||
|
|
||||||
|
// 将 nodeRecords 转换为 Map 以提高查找效率
|
||||||
|
const nodeMap: Map<string, any> = new Map(
|
||||||
|
nodeResults.map((r: any) => [r.ID, r])
|
||||||
|
);
|
||||||
|
// 将 linkRecords 转换为 Map 以提高查找效率
|
||||||
|
const linkMap: Map<string, any> = new Map(
|
||||||
|
linkResults.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;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// 时间刻度数组 (每5分钟一个刻度)
|
// 时间刻度数组 (每5分钟一个刻度)
|
||||||
const timeMarks = Array.from({ length: 288 }, (_, i) => ({
|
const timeMarks = Array.from({ length: 288 }, (_, i) => ({
|
||||||
@@ -67,14 +166,22 @@ const Timeline: React.FC<TimelineProps> = ({
|
|||||||
.padStart(2, "0")}`;
|
.padStart(2, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentTimeToDate(selectedDate: Date, minutes: number): Date {
|
||||||
|
const date = new Date(selectedDate);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const mins = minutes % 60;
|
||||||
|
date.setHours(hours, mins, 0, 0);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
// 播放时间间隔选项
|
// 播放时间间隔选项
|
||||||
const intervalOptions = [
|
const intervalOptions = [
|
||||||
{ value: 1000, label: "1秒" },
|
// { value: 1000, label: "1秒" },
|
||||||
{ value: 2000, label: "2秒" },
|
{ value: 2000, label: "2秒" },
|
||||||
{ value: 5000, label: "5秒" },
|
{ value: 5000, label: "5秒" },
|
||||||
{ value: 10000, label: "10秒" },
|
{ value: 10000, label: "10秒" },
|
||||||
];
|
];
|
||||||
// 播放时间间隔选项
|
// 强制计算时间段选项
|
||||||
const calculatedIntervalOptions = [
|
const calculatedIntervalOptions = [
|
||||||
{ value: 1440, label: "1 天" },
|
{ value: 1440, label: "1 天" },
|
||||||
{ value: 60, label: "1 小时" },
|
{ value: 60, label: "1 小时" },
|
||||||
@@ -88,79 +195,73 @@ const Timeline: React.FC<TimelineProps> = ({
|
|||||||
(event: Event, newValue: number | number[]) => {
|
(event: Event, newValue: number | number[]) => {
|
||||||
const value = Array.isArray(newValue) ? newValue[0] : newValue;
|
const value = Array.isArray(newValue) ? newValue[0] : newValue;
|
||||||
setSliderValue(value);
|
setSliderValue(value);
|
||||||
setCurrentTime(value);
|
// 防抖设置currentTime,避免频繁触发数据获取
|
||||||
onTimeChange?.(formatTime(value));
|
if (debounceRef.current) {
|
||||||
|
clearTimeout(debounceRef.current);
|
||||||
|
}
|
||||||
|
debounceRef.current = setTimeout(() => {
|
||||||
|
setCurrentTime(value);
|
||||||
|
}, 300); // 300ms 防抖延迟
|
||||||
},
|
},
|
||||||
[onTimeChange]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 播放控制
|
// 播放控制
|
||||||
const handlePlay = useCallback(() => {
|
const handlePlay = useCallback(() => {
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
onPlay?.();
|
|
||||||
|
|
||||||
intervalRef.current = setInterval(() => {
|
intervalRef.current = setInterval(() => {
|
||||||
setCurrentTime((prev) => {
|
setCurrentTime((prev) => {
|
||||||
const next = prev >= 1435 ? 0 : prev + 5; // 到达23:55后回到00:00
|
const next = prev >= 1435 ? 0 : prev + 5; // 到达23:55后回到00:00
|
||||||
setSliderValue(next);
|
setSliderValue(next);
|
||||||
onTimeChange?.(formatTime(next));
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, playInterval);
|
}, playInterval);
|
||||||
}
|
}
|
||||||
}, [isPlaying, playInterval, onPlay, onTimeChange]);
|
}, [isPlaying, playInterval]);
|
||||||
|
|
||||||
const handlePause = useCallback(() => {
|
const handlePause = useCallback(() => {
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
onPause?.();
|
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
intervalRef.current = null;
|
intervalRef.current = null;
|
||||||
}
|
}
|
||||||
}, [onPause]);
|
}, []);
|
||||||
|
|
||||||
const handleStop = useCallback(() => {
|
const handleStop = useCallback(() => {
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setCurrentTime(0);
|
setCurrentTime(0);
|
||||||
setSliderValue(0);
|
setSliderValue(0);
|
||||||
onStop?.();
|
|
||||||
onTimeChange?.(formatTime(0));
|
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
intervalRef.current = null;
|
intervalRef.current = null;
|
||||||
}
|
}
|
||||||
}, [onStop, onTimeChange]);
|
}, []);
|
||||||
|
|
||||||
// 步进控制
|
// 步进控制
|
||||||
const handleStepBackward = useCallback(() => {
|
const handleStepBackward = useCallback(() => {
|
||||||
setCurrentTime((prev) => {
|
setCurrentTime((prev) => {
|
||||||
const next = prev <= 0 ? 1435 : prev - 5;
|
const next = prev <= 0 ? 1435 : prev - 5;
|
||||||
setSliderValue(next);
|
setSliderValue(next);
|
||||||
onTimeChange?.(formatTime(next));
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, [onTimeChange]);
|
}, []);
|
||||||
|
|
||||||
const handleStepForward = useCallback(() => {
|
const handleStepForward = useCallback(() => {
|
||||||
setCurrentTime((prev) => {
|
setCurrentTime((prev) => {
|
||||||
const next = prev >= 1435 ? 0 : prev + 5;
|
const next = prev >= 1435 ? 0 : prev + 5;
|
||||||
setSliderValue(next);
|
setSliderValue(next);
|
||||||
onTimeChange?.(formatTime(next));
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, [onTimeChange]);
|
}, []);
|
||||||
|
|
||||||
// 日期选择处理
|
// 日期选择处理
|
||||||
const handleDateChange = useCallback(
|
const handleDateChange = useCallback((newDate: Date | null) => {
|
||||||
(newDate: Date | null) => {
|
if (newDate) {
|
||||||
if (newDate) {
|
setSelectedDate(newDate);
|
||||||
setSelectedDate(newDate);
|
}
|
||||||
onDateChange?.(newDate);
|
}, []);
|
||||||
}
|
|
||||||
},
|
|
||||||
[onDateChange]
|
|
||||||
);
|
|
||||||
|
|
||||||
// 播放间隔改变处理
|
// 播放间隔改变处理
|
||||||
const handleIntervalChange = useCallback(
|
const handleIntervalChange = useCallback(
|
||||||
@@ -175,25 +276,33 @@ const Timeline: React.FC<TimelineProps> = ({
|
|||||||
setCurrentTime((prev) => {
|
setCurrentTime((prev) => {
|
||||||
const next = prev >= 1435 ? 0 : prev + 5;
|
const next = prev >= 1435 ? 0 : prev + 5;
|
||||||
setSliderValue(next);
|
setSliderValue(next);
|
||||||
onTimeChange?.(formatTime(next));
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, newInterval);
|
}, newInterval);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isPlaying, onTimeChange]
|
[isPlaying]
|
||||||
);
|
);
|
||||||
// 计算时间段改变处理
|
// 计算时间段改变处理
|
||||||
const handleCalculatedIntervalChange = useCallback((event: any) => {
|
const handleCalculatedIntervalChange = useCallback((event: any) => {
|
||||||
const newInterval = event.target.value;
|
const newInterval = event.target.value;
|
||||||
setCalculatedInterval(newInterval);
|
setCalculatedInterval(newInterval);
|
||||||
}, []);
|
}, []);
|
||||||
// 组件卸载时清理定时器
|
|
||||||
|
// 添加 useEffect 来监听 currentTime 和 selectedDate 的变化,并获取数据
|
||||||
|
useEffect(() => {
|
||||||
|
fetchFrameData(currentTimeToDate(selectedDate, currentTime));
|
||||||
|
}, [currentTime, selectedDate]);
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器和防抖
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
}
|
}
|
||||||
|
if (debounceRef.current) {
|
||||||
|
clearTimeout(debounceRef.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -224,7 +333,13 @@ const Timeline: React.FC<TimelineProps> = ({
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
label="模拟数据日期选择"
|
label="模拟数据日期选择"
|
||||||
value={selectedDate}
|
value={selectedDate}
|
||||||
onChange={(newValue) => handleDateChange(newValue)}
|
onChange={(newValue) =>
|
||||||
|
handleDateChange(
|
||||||
|
newValue && "toDate" in newValue
|
||||||
|
? newValue.toDate()
|
||||||
|
: (newValue as Date | null)
|
||||||
|
)
|
||||||
|
}
|
||||||
enableAccessibleFieldDOMStructure={false}
|
enableAccessibleFieldDOMStructure={false}
|
||||||
format="yyyy-MM-dd"
|
format="yyyy-MM-dd"
|
||||||
sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }}
|
sx={{ width: 180, "& .MuiInputBase-root": { height: 40 } }}
|
||||||
@@ -308,7 +423,7 @@ const Timeline: React.FC<TimelineProps> = ({
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<Refresh />}
|
startIcon={<Refresh />}
|
||||||
onClick={onRefresh}
|
// onClick={onRefresh}
|
||||||
>
|
>
|
||||||
强制计算
|
强制计算
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -24,7 +24,24 @@ import { bearing } from "@turf/turf";
|
|||||||
import { Deck } from "@deck.gl/core";
|
import { Deck } from "@deck.gl/core";
|
||||||
import { TextLayer } from "@deck.gl/layers";
|
import { TextLayer } from "@deck.gl/layers";
|
||||||
import { TripsLayer } from "@deck.gl/geo-layers";
|
import { TripsLayer } from "@deck.gl/geo-layers";
|
||||||
import { tr } from "date-fns/locale";
|
|
||||||
|
interface MapComponentProps {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
interface DataContextType {
|
||||||
|
junctionData: any[];
|
||||||
|
pipeData: any[];
|
||||||
|
setJunctionDataState: React.Dispatch<React.SetStateAction<any[]>>;
|
||||||
|
setPipeDataState: 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
|
// 创建自定义Layer类来包装deck.gl
|
||||||
class DeckLayer extends Layer {
|
class DeckLayer extends Layer {
|
||||||
@@ -50,8 +67,9 @@ class DeckLayer extends Layer {
|
|||||||
}
|
}
|
||||||
// 跨组件传递
|
// 跨组件传递
|
||||||
const MapContext = createContext<OlMap | undefined>(undefined);
|
const MapContext = createContext<OlMap | undefined>(undefined);
|
||||||
|
const DataContext = createContext<DataContextType | undefined>(undefined);
|
||||||
|
|
||||||
const extent = config.mapExtent;
|
const extent = config.mapExtent;
|
||||||
const backendUrl = config.backendUrl;
|
|
||||||
const mapUrl = config.mapUrl;
|
const mapUrl = config.mapUrl;
|
||||||
|
|
||||||
// 添加防抖函数
|
// 添加防抖函数
|
||||||
@@ -69,16 +87,15 @@ function debounce<F extends (...args: any[]) => any>(func: F, waitFor: number) {
|
|||||||
export const useMap = () => {
|
export const useMap = () => {
|
||||||
return useContext(MapContext);
|
return useContext(MapContext);
|
||||||
};
|
};
|
||||||
|
export const useData = () => {
|
||||||
|
return useContext(DataContext);
|
||||||
|
};
|
||||||
|
|
||||||
const MapComponent: React.FC = () => {
|
const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||||
const mapRef = useRef<HTMLDivElement | null>(null);
|
const mapRef = useRef<HTMLDivElement | null>(null);
|
||||||
const deckRef = useRef<Deck | null>(null);
|
const deckRef = useRef<Deck | null>(null);
|
||||||
|
|
||||||
const [map, setMap] = useState<OlMap>();
|
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 [junctionData, setJunctionDataState] = useState<any[]>([]);
|
||||||
const [pipeData, setPipeDataState] = useState<any[]>([]);
|
const [pipeData, setPipeDataState] = useState<any[]>([]);
|
||||||
const junctionDataIds = useRef(new Set<string>());
|
const junctionDataIds = useRef(new Set<string>());
|
||||||
@@ -86,12 +103,11 @@ const MapComponent: React.FC = () => {
|
|||||||
const tileJunctionDataBuffer = useRef<any[]>([]);
|
const tileJunctionDataBuffer = useRef<any[]>([]);
|
||||||
const tilePipeDataBuffer = useRef<any[]>([]);
|
const tilePipeDataBuffer = useRef<any[]>([]);
|
||||||
|
|
||||||
let showJunctionText = true; // 控制节点文本显示
|
const [showJunctionText, setShowJunctionText] = useState(false); // 控制节点文本显示
|
||||||
let showPipeText = true; // 控制管道文本显示
|
const [showPipeText, setShowPipeText] = useState(false); // 控制管道文本显示
|
||||||
let junctionText = "pressure";
|
const [junctionText, setJunctionText] = useState("");
|
||||||
let pipeText = "flow";
|
const [pipeText, setPipeText] = useState("");
|
||||||
let animate = false; // 控制是否动画
|
const flowAnimation = useRef(true); // 添加动画控制标志
|
||||||
const isAnimating = useRef(false); // 添加动画控制标志
|
|
||||||
const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别
|
const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别
|
||||||
// 防抖更新函数
|
// 防抖更新函数
|
||||||
const debouncedUpdateData = useRef(
|
const debouncedUpdateData = useRef(
|
||||||
@@ -134,80 +150,6 @@ const MapComponent: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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" && animate) {
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (!mapRef.current) return;
|
if (!mapRef.current) return;
|
||||||
// 添加 MVT 瓦片加载逻辑
|
// 添加 MVT 瓦片加载逻辑
|
||||||
@@ -361,8 +303,12 @@ const MapComponent: React.FC = () => {
|
|||||||
value: "junctions",
|
value: "junctions",
|
||||||
type: "point",
|
type: "point",
|
||||||
properties: [
|
properties: [
|
||||||
{ name: "需求量", value: "demand" },
|
// { name: "需求量", value: "demand" },
|
||||||
{ name: "海拔高度", value: "elevation" },
|
// { name: "海拔高度", value: "elevation" },
|
||||||
|
{ name: "实际需求量", value: "actualdemand" },
|
||||||
|
{ name: "水头", value: "head" },
|
||||||
|
{ name: "压力", value: "pressure" },
|
||||||
|
{ name: "水质", value: "quality" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -378,9 +324,17 @@ const MapComponent: React.FC = () => {
|
|||||||
value: "pipes",
|
value: "pipes",
|
||||||
type: "linestring",
|
type: "linestring",
|
||||||
properties: [
|
properties: [
|
||||||
{ name: "直径", value: "diameter" },
|
// { name: "直径", value: "diameter" },
|
||||||
{ name: "粗糙度", value: "roughness" },
|
// { name: "粗糙度", value: "roughness" },
|
||||||
{ name: "局部损失", value: "minor_loss" },
|
// { 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" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -436,7 +390,7 @@ const MapComponent: React.FC = () => {
|
|||||||
const newLayers = [
|
const newLayers = [
|
||||||
new TextLayer({
|
new TextLayer({
|
||||||
id: "junctionTextLayer",
|
id: "junctionTextLayer",
|
||||||
zIndex: 1000,
|
zIndex: 10,
|
||||||
data: showJunctionText ? junctionData : [],
|
data: showJunctionText ? junctionData : [],
|
||||||
getPosition: (d: any) => d.position,
|
getPosition: (d: any) => d.position,
|
||||||
fontFamily: "Monaco, monospace",
|
fontFamily: "Monaco, monospace",
|
||||||
@@ -456,7 +410,7 @@ const MapComponent: React.FC = () => {
|
|||||||
}),
|
}),
|
||||||
new TextLayer({
|
new TextLayer({
|
||||||
id: "pipeTextLayer",
|
id: "pipeTextLayer",
|
||||||
zIndex: 1000,
|
zIndex: 10,
|
||||||
data: showPipeText ? pipeData : [],
|
data: showPipeText ? pipeData : [],
|
||||||
getPosition: (d: any) => d.position,
|
getPosition: (d: any) => d.position,
|
||||||
fontFamily: "Monaco, monospace",
|
fontFamily: "Monaco, monospace",
|
||||||
@@ -479,7 +433,7 @@ const MapComponent: React.FC = () => {
|
|||||||
|
|
||||||
// 动画循环
|
// 动画循环
|
||||||
const animate = () => {
|
const animate = () => {
|
||||||
if (!deck || !isAnimating.current) return; // 添加检查,防止空数据或停止旧循环
|
if (!deck || !flowAnimation.current) return; // 添加检查,防止空数据或停止旧循环
|
||||||
// 动画总时长(秒)
|
// 动画总时长(秒)
|
||||||
if (pipeData.length === 0) {
|
if (pipeData.length === 0) {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
@@ -496,7 +450,7 @@ const MapComponent: React.FC = () => {
|
|||||||
const waterflowLayer = new TripsLayer({
|
const waterflowLayer = new TripsLayer({
|
||||||
id: "waterflowLayer",
|
id: "waterflowLayer",
|
||||||
data: pipeData,
|
data: pipeData,
|
||||||
getPath: (d) => (isAnimating.current ? d.path : []),
|
getPath: (d) => (flowAnimation.current ? d.path : []),
|
||||||
getTimestamps: (d) => {
|
getTimestamps: (d) => {
|
||||||
return d.timestamps; // 这些应该是与 currentTime 匹配的数值
|
return d.timestamps; // 这些应该是与 currentTime 匹配的数值
|
||||||
},
|
},
|
||||||
@@ -522,35 +476,31 @@ const MapComponent: React.FC = () => {
|
|||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
};
|
};
|
||||||
animate();
|
animate();
|
||||||
}, [isAnimating, junctionData, pipeData]);
|
}, [flowAnimation, 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<MapContext.Provider value={map}>
|
<DataContext.Provider
|
||||||
<div className="relative w-full h-full">
|
value={{
|
||||||
<div ref={mapRef} className="w-full h-full"></div>
|
junctionData,
|
||||||
<MapTools />
|
pipeData,
|
||||||
</div>
|
setJunctionDataState,
|
||||||
<canvas id="deck-canvas" />
|
setPipeDataState,
|
||||||
</MapContext.Provider>
|
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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import BaseLayers from "./Controls/BaseLayers";
|
|||||||
import MapToolbar from "./Controls/Toolbar";
|
import MapToolbar from "./Controls/Toolbar";
|
||||||
import ScaleLine from "./Controls/ScaleLine";
|
import ScaleLine from "./Controls/ScaleLine";
|
||||||
import LayerControl from "./Controls/LayerControl";
|
import LayerControl from "./Controls/LayerControl";
|
||||||
|
interface MapToolsProps {}
|
||||||
|
|
||||||
const MapTools = () => {
|
const MapTools: React.FC<MapToolsProps> = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Zoom />
|
<Zoom />
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
py: 6,
|
py: 6,
|
||||||
color: "text.secondary",
|
color: "text.secondary",
|
||||||
|
height: 376,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" gutterBottom>
|
<Typography variant="h6" gutterBottom>
|
||||||
@@ -307,7 +308,7 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
const chartSection = hasData ? (
|
const chartSection = hasData ? (
|
||||||
<LineChart
|
<LineChart
|
||||||
dataset={dataset}
|
dataset={dataset}
|
||||||
height={360}
|
height={376}
|
||||||
margin={{ left: 50, right: 50, top: 20, bottom: 80 }}
|
margin={{ left: 50, right: 50, top: 20, bottom: 80 }}
|
||||||
xAxis={[
|
xAxis={[
|
||||||
{
|
{
|
||||||
@@ -354,8 +355,8 @@ const SCADADataPanel: React.FC<SCADADataPanelProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"absolute right-4 top-20 w-4xl h-2xl bg-white/95 backdrop-blur-[10px] rounded-xl shadow-lg overflow-hidden flex flex-col transition-opacity duration-300",
|
"absolute right-4 top-20 w-4xl h-2xl bg-white rounded-xl shadow-lg overflow-hidden flex flex-col transition-opacity duration-300",
|
||||||
visible ? "opacity-95" : "opacity-0"
|
visible ? "opacity-95 hover:opacity-100" : "opacity-0 -z-10"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="absolute left-4 top-20 w-90 max-h-[calc(100vh-100px)] bg-white/95 backdrop-blur-[10px] rounded-xl shadow-lg overflow-hidden flex flex-col opacity-95 transition-opacity duration-200 ease-in-out hover:opacity-100">
|
<Paper className="absolute left-4 top-20 w-90 max-h-[calc(100vh-100px)] bg-white rounded-xl shadow-lg overflow-hidden flex flex-col opacity-95 transition-opacity duration-200 ease-in-out hover:opacity-100">
|
||||||
{/* 头部控制栏 */}
|
{/* 头部控制栏 */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -228,7 +228,7 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
<Box sx={{ p: 2, backgroundColor: "grey.50" }}>
|
<Box sx={{ p: 2, backgroundColor: "grey.50" }}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
{/* 搜索框 */}
|
{/* 搜索框 */}
|
||||||
<Box className="h-10 flex items-center border border-gray-300 rounded-lg p-0.5">
|
<Box className="h-10 flex items-center border border-gray-300 rounded-md p-0.5">
|
||||||
<InputBase
|
<InputBase
|
||||||
sx={{ ml: 1, flex: 1 }}
|
sx={{ ml: 1, flex: 1 }}
|
||||||
placeholder="搜索设备名称、ID 或类型..."
|
placeholder="搜索设备名称、ID 或类型..."
|
||||||
@@ -395,6 +395,11 @@ const SCADADeviceList: React.FC<SCADADeviceListProps> = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
|
slotProps={{
|
||||||
|
secondary: {
|
||||||
|
component: "div", // 使其支持多行
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tooltip title="缩放到设备位置">
|
<Tooltip title="缩放到设备位置">
|
||||||
|
|||||||
@@ -151,4 +151,27 @@ function jenks_with_stratified_sampling(data, n_classes, sample_size = 10000) {
|
|||||||
return jenks_breaks_jenkspy(sampled_data, n_classes);
|
return jenks_breaks_jenkspy(sampled_data, n_classes);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { prettyBreaksClassification, jenks_breaks_jenkspy, jenks_with_stratified_sampling };
|
/**
|
||||||
|
* 根据指定的方法计算数据的分类断点。
|
||||||
|
* @param {Array<number>} data - 要分类的数值数据数组。
|
||||||
|
* @param {number} segments - 要创建的段数或类别数。
|
||||||
|
* @param {string} classificationMethod - 要使用的分类方法。支持的值:"pretty_breaks" 或 "jenks_optimized"。
|
||||||
|
* @returns {Array<number>} 分类的断点数组。如果数据为空或无效,则返回空数组。
|
||||||
|
*/
|
||||||
|
function calculateClassification(
|
||||||
|
data,
|
||||||
|
segments,
|
||||||
|
classificationMethod
|
||||||
|
) {
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (classificationMethod === "pretty_breaks") {
|
||||||
|
return prettyBreaksClassification(data, segments);
|
||||||
|
}
|
||||||
|
if (classificationMethod === "jenks_optimized") {
|
||||||
|
return jenks_with_stratified_sampling(data, segments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { prettyBreaksClassification, jenks_breaks_jenkspy, jenks_with_stratified_sampling, calculateClassification };
|
||||||
35
src/utils/parseColor.js
Normal file
35
src/utils/parseColor.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 将颜色字符串解析为包含红色、绿色、蓝色和 alpha 分量的对象。
|
||||||
|
* 支持 rgba、rgb 和十六进制颜色格式。对于 rgba 和 rgb,提取 r、g、b 和 a(如果未提供,则默认为 1)。
|
||||||
|
* 对于十六进制(例如 #RRGGBB),提取 r、g、b。(e.g., "rgba(255, 0, 0, 0.5)", "rgb(255, 0, 0)", or "#FF0000").
|
||||||
|
* @param {string} color - 要解析的颜色字符串(例如 "rgba(255, 0, 0, 0.5)"、"rgb(255, 0, 0)" 或 "#FF0000")。
|
||||||
|
* @returns {{r: number, g: number, b: number, a?: number}} 包含颜色分量的对象:
|
||||||
|
* - r: 红色分量 (0-255)
|
||||||
|
* - g: 绿色分量 (0-255)
|
||||||
|
* - b: 蓝色分量 (0-255)
|
||||||
|
* - a: Alpha 分量 (0-1),如果未指定则默认为 1
|
||||||
|
**/
|
||||||
|
function parseColor(color) {
|
||||||
|
// 解析 rgba 格式的颜色
|
||||||
|
const match = color.match(
|
||||||
|
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/
|
||||||
|
);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
r: parseInt(match[1], 10),
|
||||||
|
g: parseInt(match[2], 10),
|
||||||
|
b: parseInt(match[3], 10),
|
||||||
|
// 如果没有 alpha 值,默认为 1
|
||||||
|
a: match[4] ? parseFloat(match[4]) : 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 如果还是十六进制格式,保持原来的解析方式
|
||||||
|
const hex = color.replace("#", "");
|
||||||
|
return {
|
||||||
|
r: parseInt(hex.slice(0, 2), 16),
|
||||||
|
g: parseInt(hex.slice(2, 4), 16),
|
||||||
|
b: parseInt(hex.slice(4, 6), 16),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { parseColor };
|
||||||
Reference in New Issue
Block a user