持久化样式状态;分离样式设置和图例显示;完成时间轴组件代码修复建议
This commit is contained in:
@@ -22,7 +22,7 @@ import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
|||||||
import VectorTileSource from "ol/source/VectorTile";
|
import VectorTileSource from "ol/source/VectorTile";
|
||||||
import { useData, useMap } from "../MapComponent";
|
import { useData, useMap } from "../MapComponent";
|
||||||
|
|
||||||
import StyleLegend, { LegendStyleConfig } from "./StyleLegend";
|
import { LegendStyleConfig } from "./StyleLegend";
|
||||||
import { FlatStyleLike } from "ol/style/flat";
|
import { FlatStyleLike } from "ol/style/flat";
|
||||||
|
|
||||||
import { calculateClassification } from "@utils/breaks_classification";
|
import { calculateClassification } from "@utils/breaks_classification";
|
||||||
@@ -55,7 +55,10 @@ interface LayerStyleState {
|
|||||||
legendConfig: LegendStyleConfig;
|
legendConfig: LegendStyleConfig;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
// 持久化存储
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
layerStyleStates: "styleEditor_layerStyleStates",
|
||||||
|
};
|
||||||
// 预设颜色方案
|
// 预设颜色方案
|
||||||
const SINGLE_COLOR_PALETTES = [
|
const SINGLE_COLOR_PALETTES = [
|
||||||
{
|
{
|
||||||
@@ -116,6 +119,7 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
setShowPipeText,
|
setShowPipeText,
|
||||||
setJunctionText,
|
setJunctionText,
|
||||||
setPipeText,
|
setPipeText,
|
||||||
|
updateLegendConfigs,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const { open, close } = useNotification();
|
const { open, close } = useNotification();
|
||||||
@@ -128,11 +132,6 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
const [renderLayers, setRenderLayers] = useState<WebGLVectorTileLayer[]>([]);
|
const [renderLayers, setRenderLayers] = useState<WebGLVectorTileLayer[]>([]);
|
||||||
const [selectedRenderLayer, setSelectedRenderLayer] =
|
const [selectedRenderLayer, setSelectedRenderLayer] =
|
||||||
useState<WebGLVectorTileLayer>();
|
useState<WebGLVectorTileLayer>();
|
||||||
const [selectedProperty, setSelectedProperty] = useState<{
|
|
||||||
name: string;
|
|
||||||
value: string;
|
|
||||||
}>({ name: "", value: "" });
|
|
||||||
|
|
||||||
const [availableProperties, setAvailableProperties] = useState<
|
const [availableProperties, setAvailableProperties] = useState<
|
||||||
{ name: string; value: string }[]
|
{ name: string; value: string }[]
|
||||||
>([]);
|
>([]);
|
||||||
@@ -154,8 +153,18 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
});
|
});
|
||||||
// 样式状态管理 - 存储多个图层的样式状态
|
// 样式状态管理 - 存储多个图层的样式状态
|
||||||
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>(
|
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>(
|
||||||
[]
|
() => {
|
||||||
|
const saved = sessionStorage.getItem(STORAGE_KEYS.layerStyleStates);
|
||||||
|
return saved ? JSON.parse(saved) : [];
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
// 保存layerStyleStates到sessionStorage
|
||||||
|
useEffect(() => {
|
||||||
|
sessionStorage.setItem(
|
||||||
|
STORAGE_KEYS.layerStyleStates,
|
||||||
|
JSON.stringify(layerStyleStates)
|
||||||
|
);
|
||||||
|
}, [layerStyleStates]);
|
||||||
// 颜色方案选择
|
// 颜色方案选择
|
||||||
const [singlePaletteIndex, setSinglePaletteIndex] = useState(0);
|
const [singlePaletteIndex, setSinglePaletteIndex] = useState(0);
|
||||||
const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0);
|
const [gradientPaletteIndex, setGradientPaletteIndex] = useState(0);
|
||||||
@@ -186,31 +195,37 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
);
|
);
|
||||||
// 保存当前图层的样式状态
|
// 保存当前图层的样式状态
|
||||||
const saveLayerStyle = useCallback(
|
const saveLayerStyle = useCallback(
|
||||||
(layerId?: string, legendConfig?: LegendStyleConfig) => {
|
(layerId?: string, newLegendConfig?: LegendStyleConfig) => {
|
||||||
if (!selectedRenderLayer || !styleConfig.property) {
|
if (!selectedRenderLayer || !styleConfig.property) {
|
||||||
console.warn("无法保存样式:缺少必要的图层或样式配置");
|
console.warn("无法保存样式:缺少必要的图层或样式配置");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!layerId) return; // 如果没有传入 layerId,则不保存
|
if (!layerId) return; // 如果没有传入 layerId,则不保存
|
||||||
const layerName = selectedRenderLayer.get("name") || `图层${layerId}`;
|
|
||||||
// 如果没有传入图例配置,则创建一个默认的空配置
|
// 如果没有传入图例配置,则创建一个默认的空配置
|
||||||
const finalLegendConfig: LegendStyleConfig = legendConfig || {
|
const layerName =
|
||||||
|
newLegendConfig?.layerName ||
|
||||||
|
selectedRenderLayer.get("name") ||
|
||||||
|
`图层${layerId}`;
|
||||||
|
const property = availableProperties.find(
|
||||||
|
(p) => p.value === styleConfig.property
|
||||||
|
);
|
||||||
|
let legendConfig: LegendStyleConfig = newLegendConfig || {
|
||||||
layerId,
|
layerId,
|
||||||
layerName,
|
layerName,
|
||||||
property: selectedProperty.name,
|
property: property?.name || styleConfig.property,
|
||||||
colors: [],
|
colors: [],
|
||||||
type: "point",
|
type: selectedRenderLayer.get("type"),
|
||||||
dimensions: [],
|
dimensions: [],
|
||||||
breaks: [],
|
breaks: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const newStyleState: LayerStyleState = {
|
const newStyleState: LayerStyleState = {
|
||||||
layerId,
|
layerId,
|
||||||
layerName,
|
layerName,
|
||||||
styleConfig: { ...styleConfig },
|
styleConfig: { ...styleConfig },
|
||||||
legendConfig: { ...finalLegendConfig },
|
legendConfig: { ...legendConfig },
|
||||||
isActive: true,
|
isActive: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
setLayerStyleStates((prev) => {
|
setLayerStyleStates((prev) => {
|
||||||
// 检查是否已存在该图层的样式状态
|
// 检查是否已存在该图层的样式状态
|
||||||
const existingIndex = prev.findIndex(
|
const existingIndex = prev.findIndex(
|
||||||
@@ -323,12 +338,12 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const styleConfig = layerStyleConfig.styleConfig;
|
const styleConfig = layerStyleConfig.styleConfig;
|
||||||
const selectedRenderLayer = renderLayers.filter((layer) => {
|
const renderLayer = renderLayers.filter((layer) => {
|
||||||
return layer.get("value") === layerStyleConfig.layerId;
|
return layer.get("value") === layerStyleConfig.layerId;
|
||||||
})[0];
|
})[0];
|
||||||
if (!selectedRenderLayer || !styleConfig?.property) return;
|
if (!renderLayer || !styleConfig?.property) return;
|
||||||
const layerType: string = selectedRenderLayer?.get("type");
|
const layerType: string = renderLayer?.get("type");
|
||||||
const source = selectedRenderLayer.getSource();
|
const source = renderLayer.getSource();
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
|
|
||||||
const breaksLength = breaks.length;
|
const breaksLength = breaks.length;
|
||||||
@@ -416,13 +431,17 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
dynamicStyle["circle-stroke-width"] = 2;
|
dynamicStyle["circle-stroke-width"] = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedRenderLayer.setStyle(dynamicStyle);
|
renderLayer.setStyle(dynamicStyle);
|
||||||
|
// 用初始化时的样式配置更新图例配置,避免覆盖已有的图例名称和属性
|
||||||
|
const layerId = renderLayer.get("value");
|
||||||
|
const initLayerStyleState = layerStyleStates.find(
|
||||||
|
(s) => s.layerId === layerId
|
||||||
|
);
|
||||||
// 创建图例配置对象
|
// 创建图例配置对象
|
||||||
const legendConfig: LegendStyleConfig = {
|
const legendConfig: LegendStyleConfig = {
|
||||||
layerName: selectedRenderLayer.get("name"),
|
layerName: initLayerStyleState?.layerName || `图层${layerId}`,
|
||||||
layerId: selectedRenderLayer.get("value"),
|
layerId: layerId,
|
||||||
property: selectedProperty.name,
|
property: initLayerStyleState?.legendConfig.property || "",
|
||||||
colors: colors,
|
colors: colors,
|
||||||
type: layerType,
|
type: layerType,
|
||||||
dimensions: dimensions,
|
dimensions: dimensions,
|
||||||
@@ -430,7 +449,7 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
};
|
};
|
||||||
// 自动保存样式状态,直接传入图例配置
|
// 自动保存样式状态,直接传入图例配置
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
saveLayerStyle(selectedRenderLayer.get("value"), legendConfig);
|
saveLayerStyle(renderLayer.get("value"), legendConfig);
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
// 重置样式
|
// 重置样式
|
||||||
@@ -695,20 +714,7 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
setStyleConfig(cachedStyleState.styleConfig);
|
setStyleConfig(cachedStyleState.styleConfig);
|
||||||
}
|
}
|
||||||
}, [renderLayers, selectedRenderLayer, map, renderLayers, layerStyleStates]);
|
}, [renderLayers, selectedRenderLayer, map, renderLayers, layerStyleStates]);
|
||||||
// 同步属性状态
|
|
||||||
useEffect(() => {
|
|
||||||
// 当属性值变化时自动同步 selectedProperty
|
|
||||||
if (styleConfig.property) {
|
|
||||||
const prop = availableProperties.find(
|
|
||||||
(p) => p.value === styleConfig.property
|
|
||||||
);
|
|
||||||
if (prop) {
|
|
||||||
setSelectedProperty({ name: prop.name, value: prop.value });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setSelectedProperty({ name: "", value: "" });
|
|
||||||
}
|
|
||||||
}, [styleConfig.property, availableProperties]);
|
|
||||||
// 监听颜色类型变化,当切换到单一色时自动勾选宽度调整选项
|
// 监听颜色类型变化,当切换到单一色时自动勾选宽度调整选项
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (styleConfig.colorType === "single") {
|
if (styleConfig.colorType === "single") {
|
||||||
@@ -720,14 +726,17 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
}, [styleConfig.colorType]);
|
}, [styleConfig.colorType]);
|
||||||
|
|
||||||
// 获取所有激活的图例配置
|
// 获取所有激活的图例配置
|
||||||
const getActiveLegendConfigs = useCallback(() => {
|
useEffect(() => {
|
||||||
return layerStyleStates
|
if (!updateLegendConfigs) return;
|
||||||
.filter((state) => state.isActive && state.legendConfig.property)
|
updateLegendConfigs(
|
||||||
.map((state) => ({
|
layerStyleStates
|
||||||
...state.legendConfig,
|
.filter((state) => state.isActive && state.legendConfig.property)
|
||||||
layerName: state.layerName,
|
.map((state) => ({
|
||||||
layerId: state.layerId,
|
...state.legendConfig,
|
||||||
}));
|
layerName: state.layerName,
|
||||||
|
layerId: state.layerId,
|
||||||
|
}))
|
||||||
|
);
|
||||||
}, [layerStyleStates]);
|
}, [layerStyleStates]);
|
||||||
|
|
||||||
const getColorSetting = () => {
|
const getColorSetting = () => {
|
||||||
@@ -1184,16 +1193,6 @@ const StyleEditorPanel: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
{/* 显示多图层图例 */}
|
|
||||||
{getActiveLegendConfigs().length > 0 && (
|
|
||||||
<div className=" absolute bottom-40 right-4 drop-shadow-xl flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
|
|
||||||
<div className="flex flex-row gap-3">
|
|
||||||
{getActiveLegendConfigs().map((config, index) => (
|
|
||||||
<StyleLegend key={`${config.layerId}-${index}`} {...config} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ const Timeline: React.FC = () => {
|
|||||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒
|
const [playInterval, setPlayInterval] = useState<number>(5000); // 毫秒
|
||||||
const [calculatedInterval, setCalculatedInterval] = useState<number>(1440); // 分钟
|
const [calculatedInterval, setCalculatedInterval] = useState<number>(1440); // 分钟
|
||||||
const [sliderValue, setSliderValue] = useState<number>(0);
|
|
||||||
|
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const timelineRef = useRef<HTMLDivElement>(null);
|
const timelineRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -192,7 +191,6 @@ const Timeline: React.FC = () => {
|
|||||||
const handleSliderChange = useCallback(
|
const handleSliderChange = useCallback(
|
||||||
(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);
|
|
||||||
// 防抖设置currentTime,避免频繁触发数据获取
|
// 防抖设置currentTime,避免频繁触发数据获取
|
||||||
if (debounceRef.current) {
|
if (debounceRef.current) {
|
||||||
clearTimeout(debounceRef.current);
|
clearTimeout(debounceRef.current);
|
||||||
@@ -219,7 +217,6 @@ const Timeline: React.FC = () => {
|
|||||||
intervalRef.current = setInterval(() => {
|
intervalRef.current = setInterval(() => {
|
||||||
setCurrentTime((prev) => {
|
setCurrentTime((prev) => {
|
||||||
const next = prev >= 1440 ? 0 : prev + 15; // 到达24:00后回到00:00
|
const next = prev >= 1440 ? 0 : prev + 15; // 到达24:00后回到00:00
|
||||||
setSliderValue(next);
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, playInterval);
|
}, playInterval);
|
||||||
@@ -237,7 +234,6 @@ const Timeline: React.FC = () => {
|
|||||||
const handleStop = useCallback(() => {
|
const handleStop = useCallback(() => {
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setCurrentTime(0);
|
setCurrentTime(0);
|
||||||
setSliderValue(0);
|
|
||||||
if (intervalRef.current) {
|
if (intervalRef.current) {
|
||||||
clearInterval(intervalRef.current);
|
clearInterval(intervalRef.current);
|
||||||
intervalRef.current = null;
|
intervalRef.current = null;
|
||||||
@@ -248,7 +244,6 @@ const Timeline: React.FC = () => {
|
|||||||
const handleStepBackward = useCallback(() => {
|
const handleStepBackward = useCallback(() => {
|
||||||
setCurrentTime((prev) => {
|
setCurrentTime((prev) => {
|
||||||
const next = prev <= 0 ? 1440 : prev - 15;
|
const next = prev <= 0 ? 1440 : prev - 15;
|
||||||
setSliderValue(next);
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -256,7 +251,6 @@ const Timeline: React.FC = () => {
|
|||||||
const handleStepForward = useCallback(() => {
|
const handleStepForward = useCallback(() => {
|
||||||
setCurrentTime((prev) => {
|
setCurrentTime((prev) => {
|
||||||
const next = prev >= 1440 ? 0 : prev + 15;
|
const next = prev >= 1440 ? 0 : prev + 15;
|
||||||
setSliderValue(next);
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -280,7 +274,6 @@ const Timeline: React.FC = () => {
|
|||||||
intervalRef.current = setInterval(() => {
|
intervalRef.current = setInterval(() => {
|
||||||
setCurrentTime((prev) => {
|
setCurrentTime((prev) => {
|
||||||
const next = prev >= 1440 ? 0 : prev + 15;
|
const next = prev >= 1440 ? 0 : prev + 15;
|
||||||
setSliderValue(next);
|
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, newInterval);
|
}, newInterval);
|
||||||
@@ -485,7 +478,7 @@ const Timeline: React.FC = () => {
|
|||||||
{/* 时间轴滑块 */}
|
{/* 时间轴滑块 */}
|
||||||
<Box ref={timelineRef} sx={{ px: 2 }}>
|
<Box ref={timelineRef} sx={{ px: 2 }}>
|
||||||
<Slider
|
<Slider
|
||||||
value={sliderValue}
|
value={currentTime}
|
||||||
min={0}
|
min={0}
|
||||||
max={1440} // 24:00 = 1440分钟
|
max={1440} // 24:00 = 1440分钟
|
||||||
step={15} // 每15分钟一个步进
|
step={15} // 每15分钟一个步进
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { TextLayer } from "@deck.gl/layers";
|
|||||||
import { TripsLayer } from "@deck.gl/geo-layers";
|
import { TripsLayer } from "@deck.gl/geo-layers";
|
||||||
import { CollisionFilterExtension } from "@deck.gl/extensions";
|
import { CollisionFilterExtension } from "@deck.gl/extensions";
|
||||||
|
|
||||||
|
import StyleLegend from "./Controls/StyleLegend"; // 假设 StyleLegend 在同一目录
|
||||||
|
|
||||||
interface MapComponentProps {
|
interface MapComponentProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -45,6 +47,7 @@ interface DataContextType {
|
|||||||
pipeText: string;
|
pipeText: string;
|
||||||
setJunctionText?: React.Dispatch<React.SetStateAction<string>>;
|
setJunctionText?: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setPipeText?: React.Dispatch<React.SetStateAction<string>>;
|
setPipeText?: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
updateLegendConfigs?: (configs: any[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建自定义Layer类来包装deck.gl
|
// 创建自定义Layer类来包装deck.gl
|
||||||
@@ -125,6 +128,12 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
const [pipeText, setPipeText] = useState("");
|
const [pipeText, setPipeText] = useState("");
|
||||||
const flowAnimation = useRef(false); // 添加动画控制标志
|
const flowAnimation = useRef(false); // 添加动画控制标志
|
||||||
const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别
|
const [currentZoom, setCurrentZoom] = useState(12); // 当前缩放级别
|
||||||
|
// 图例配置
|
||||||
|
const [activeLegendConfigs, setActiveLegendConfigs] = useState<any[]>([]); // 存储图例配置
|
||||||
|
// 从 StyleEditorPanel 接收图例配置的回调
|
||||||
|
const updateLegendConfigs = (configs: any[]) => {
|
||||||
|
setActiveLegendConfigs(configs);
|
||||||
|
};
|
||||||
// 防抖更新函数
|
// 防抖更新函数
|
||||||
const debouncedUpdateData = useRef(
|
const debouncedUpdateData = useRef(
|
||||||
debounce(() => {
|
debounce(() => {
|
||||||
@@ -616,6 +625,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
showPipeText,
|
showPipeText,
|
||||||
junctionText,
|
junctionText,
|
||||||
pipeText,
|
pipeText,
|
||||||
|
updateLegendConfigs,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MapContext.Provider value={map}>
|
<MapContext.Provider value={map}>
|
||||||
@@ -626,6 +636,16 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
<canvas id="deck-canvas" />
|
<canvas id="deck-canvas" />
|
||||||
</MapContext.Provider>
|
</MapContext.Provider>
|
||||||
|
{/* 图例始终渲染 */}
|
||||||
|
{activeLegendConfigs.length > 0 && (
|
||||||
|
<div className="absolute bottom-40 right-4 drop-shadow-xl flex flex-row items-end max-w-screen-lg overflow-x-auto z-10">
|
||||||
|
<div className="flex flex-row gap-3">
|
||||||
|
{activeLegendConfigs.map((config, index) => (
|
||||||
|
<StyleLegend key={`${config.layerId}-${index}`} {...config} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DataContext.Provider>
|
</DataContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user