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