新增自动应用样式功能;强制计算后自动应用默认样式
This commit is contained in:
@@ -0,0 +1,590 @@
|
||||
import ApplyIcon from "@mui/icons-material/Check";
|
||||
import ColorLensIcon from "@mui/icons-material/ColorLens";
|
||||
import ResetIcon from "@mui/icons-material/Refresh";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Slider,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
CLASSIFICATION_METHODS,
|
||||
COLOR_TYPE_OPTIONS,
|
||||
GRADIENT_PALETTES,
|
||||
RAINBOW_PALETTES,
|
||||
SINGLE_COLOR_PALETTES,
|
||||
} from "./styleEditorPresets";
|
||||
import { StyleEditorFormProps } from "./styleEditorTypes";
|
||||
import {
|
||||
getSizePreviewColors,
|
||||
hexToRgba,
|
||||
resolveStyleColors,
|
||||
rgbaToHex,
|
||||
} from "./styleEditorUtils";
|
||||
|
||||
const StyleEditorForm: React.FC<StyleEditorFormProps> = ({
|
||||
renderLayers,
|
||||
selectedRenderLayer,
|
||||
styleConfig,
|
||||
setStyleConfig,
|
||||
availableProperties,
|
||||
onLayerChange,
|
||||
onPropertyChange,
|
||||
onClassificationMethodChange,
|
||||
onSegmentsChange,
|
||||
onCustomBreakChange,
|
||||
onCustomBreakBlur,
|
||||
onColorTypeChange,
|
||||
onApply,
|
||||
onReset,
|
||||
}) => {
|
||||
const renderColorSetting = () => {
|
||||
if (styleConfig.colorType === "single") {
|
||||
return (
|
||||
<FormControl variant="standard" fullWidth margin="dense" className="mt-3">
|
||||
<InputLabel>单一色方案</InputLabel>
|
||||
<Select
|
||||
value={styleConfig.singlePaletteIndex}
|
||||
onChange={(e) =>
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
singlePaletteIndex: Number(e.target.value),
|
||||
}))
|
||||
}
|
||||
>
|
||||
{SINGLE_COLOR_PALETTES.map((palette, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
<Box width="100%" sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "80%",
|
||||
height: 16,
|
||||
borderRadius: 2,
|
||||
background: palette.color,
|
||||
marginRight: 1,
|
||||
border: "1px solid #ccc",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
||||
if (styleConfig.colorType === "gradient") {
|
||||
return (
|
||||
<FormControl variant="standard" fullWidth margin="dense" className="mt-3">
|
||||
<InputLabel>渐进色方案</InputLabel>
|
||||
<Select
|
||||
value={styleConfig.gradientPaletteIndex}
|
||||
onChange={(e) =>
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
gradientPaletteIndex: Number(e.target.value),
|
||||
}))
|
||||
}
|
||||
>
|
||||
{GRADIENT_PALETTES.map((palette, index) => {
|
||||
const previewColors = resolveStyleColors(
|
||||
{ ...styleConfig, colorType: "gradient", gradientPaletteIndex: index },
|
||||
styleConfig.segments + 1
|
||||
);
|
||||
return (
|
||||
<MenuItem key={index} value={index}>
|
||||
<Box width="100%" sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "80%",
|
||||
height: 16,
|
||||
borderRadius: 2,
|
||||
display: "flex",
|
||||
overflow: "hidden",
|
||||
marginRight: 1,
|
||||
border: "1px solid #ccc",
|
||||
}}
|
||||
>
|
||||
{previewColors.map((color, colorIndex) => (
|
||||
<Box
|
||||
key={colorIndex}
|
||||
sx={{ flex: 1, backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
||||
if (styleConfig.colorType === "rainbow") {
|
||||
return (
|
||||
<FormControl variant="standard" fullWidth margin="dense" className="mt-3">
|
||||
<InputLabel>离散彩虹方案</InputLabel>
|
||||
<Select
|
||||
value={styleConfig.rainbowPaletteIndex}
|
||||
onChange={(e) =>
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
rainbowPaletteIndex: Number(e.target.value),
|
||||
}))
|
||||
}
|
||||
>
|
||||
{RAINBOW_PALETTES.map((palette, index) => {
|
||||
const previewColors = Array.from(
|
||||
{ length: styleConfig.segments + 1 },
|
||||
(_, colorIndex) => palette.colors[colorIndex % palette.colors.length]
|
||||
);
|
||||
return (
|
||||
<MenuItem key={index} value={index}>
|
||||
<Box width="100%" sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography sx={{ marginRight: 1 }}>{palette.name}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: "60%",
|
||||
height: 16,
|
||||
borderRadius: 2,
|
||||
display: "flex",
|
||||
border: "1px solid #ccc",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{previewColors.map((color, colorIndex) => (
|
||||
<Box
|
||||
key={colorIndex}
|
||||
sx={{ flex: 1, backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
||||
if (styleConfig.colorType === "custom") {
|
||||
return (
|
||||
<Box className="mt-3">
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
自定义颜色
|
||||
</Typography>
|
||||
<Box
|
||||
className="flex flex-col gap-2"
|
||||
sx={{ maxHeight: "160px", overflowY: "auto", paddingTop: "4px" }}
|
||||
>
|
||||
{Array.from({ length: styleConfig.segments }).map((_, index) => {
|
||||
const color = styleConfig.customColors?.[index] || "rgba(0,0,0,1)";
|
||||
return (
|
||||
<Box key={index} className="flex items-center gap-2">
|
||||
<Typography variant="caption" sx={{ width: 40 }}>
|
||||
分段{index + 1}
|
||||
</Typography>
|
||||
<input
|
||||
type="color"
|
||||
value={rgbaToHex(color)}
|
||||
onChange={(e) => {
|
||||
const nextColor = hexToRgba(e.target.value);
|
||||
setStyleConfig((prev) => {
|
||||
const nextColors = [...(prev.customColors || [])];
|
||||
while (nextColors.length < prev.segments) {
|
||||
nextColors.push("rgba(0,0,0,1)");
|
||||
}
|
||||
nextColors[index] = nextColor;
|
||||
return { ...prev, customColors: nextColors };
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "32px",
|
||||
cursor: "pointer",
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderSizeSetting = () => {
|
||||
const previewColors = getSizePreviewColors(styleConfig);
|
||||
|
||||
if (selectedRenderLayer?.get("type") === "point") {
|
||||
return (
|
||||
<Box className="mt-3">
|
||||
<Typography gutterBottom>
|
||||
点大小范围: {styleConfig.minSize} - {styleConfig.maxSize} 像素
|
||||
</Typography>
|
||||
<Box className="flex items-center gap-4">
|
||||
<Box className="flex-1">
|
||||
<Typography variant="caption" gutterBottom>
|
||||
最小值
|
||||
</Typography>
|
||||
<Slider
|
||||
value={styleConfig.minSize}
|
||||
onChange={(_, value) =>
|
||||
setStyleConfig((prev) => ({ ...prev, minSize: value as number }))
|
||||
}
|
||||
min={2}
|
||||
max={8}
|
||||
step={1}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-1">
|
||||
<Typography variant="caption" gutterBottom>
|
||||
最大值
|
||||
</Typography>
|
||||
<Slider
|
||||
value={styleConfig.maxSize}
|
||||
onChange={(_, value) =>
|
||||
setStyleConfig((prev) => ({ ...prev, maxSize: value as number }))
|
||||
}
|
||||
min={10}
|
||||
max={16}
|
||||
step={1}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className="flex items-center gap-2 mt-2 p-2 bg-gray-50 rounded">
|
||||
<Typography variant="caption">预览:</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: styleConfig.minSize,
|
||||
height: styleConfig.minSize,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: previewColors[0],
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption">到</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: styleConfig.maxSize,
|
||||
height: styleConfig.maxSize,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: previewColors[previewColors.length - 1],
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedRenderLayer?.get("type") === "linestring") {
|
||||
return (
|
||||
<Box className="mt-3">
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={styleConfig.adjustWidthByProperty}
|
||||
onChange={(e) =>
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
adjustWidthByProperty: e.target.checked,
|
||||
}))
|
||||
}
|
||||
disabled={styleConfig.colorType === "single"}
|
||||
/>
|
||||
}
|
||||
label="根据数值分段调整线条宽度"
|
||||
/>
|
||||
{styleConfig.adjustWidthByProperty ? (
|
||||
<>
|
||||
<Typography gutterBottom>
|
||||
线条宽度范围: {styleConfig.minStrokeWidth} - {styleConfig.maxStrokeWidth}
|
||||
px
|
||||
</Typography>
|
||||
<Box className="flex items-center gap-4">
|
||||
<Box className="flex-1">
|
||||
<Typography variant="caption" gutterBottom>
|
||||
最小值
|
||||
</Typography>
|
||||
<Slider
|
||||
value={styleConfig.minStrokeWidth}
|
||||
onChange={(_, value) =>
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
minStrokeWidth: value as number,
|
||||
}))
|
||||
}
|
||||
min={1}
|
||||
max={4}
|
||||
step={0.5}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
<Box className="flex-1">
|
||||
<Typography variant="caption" gutterBottom>
|
||||
最大值
|
||||
</Typography>
|
||||
<Slider
|
||||
value={styleConfig.maxStrokeWidth}
|
||||
onChange={(_, value) =>
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
maxStrokeWidth: value as number,
|
||||
}))
|
||||
}
|
||||
min={6}
|
||||
max={12}
|
||||
step={0.5}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className="flex items-center gap-2 mt-2 p-2 bg-gray-50 rounded">
|
||||
<Typography variant="caption">预览:</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: 50,
|
||||
height: styleConfig.minStrokeWidth,
|
||||
backgroundColor: previewColors[0],
|
||||
border: `1px solid ${previewColors[0]}`,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="caption">到</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: 50,
|
||||
height: styleConfig.maxStrokeWidth,
|
||||
backgroundColor: previewColors[previewColors.length - 1],
|
||||
border: `1px solid ${previewColors[previewColors.length - 1]}`,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography gutterBottom>
|
||||
固定线条宽度: {styleConfig.fixedStrokeWidth}px
|
||||
</Typography>
|
||||
<Slider
|
||||
value={styleConfig.fixedStrokeWidth}
|
||||
onChange={(_, value) =>
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
fixedStrokeWidth: value as number,
|
||||
}))
|
||||
}
|
||||
min={1}
|
||||
max={10}
|
||||
step={0.5}
|
||||
size="small"
|
||||
/>
|
||||
<Box className="flex items-center gap-2 mt-2 p-2 bg-gray-50 rounded">
|
||||
<Typography variant="caption">预览:</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
width: 50,
|
||||
height: styleConfig.fixedStrokeWidth,
|
||||
backgroundColor: previewColors[0],
|
||||
border: `1px solid ${previewColors[0]}`,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
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 z-1300">
|
||||
<FormControl variant="standard" fullWidth margin="dense">
|
||||
<InputLabel>选择图层</InputLabel>
|
||||
<Select
|
||||
value={selectedRenderLayer ? renderLayers.indexOf(selectedRenderLayer) : ""}
|
||||
onChange={(e) => onLayerChange(e.target.value as number)}
|
||||
>
|
||||
{renderLayers.map((layer, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
{layer.get("name")}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl variant="standard" fullWidth margin="dense">
|
||||
<InputLabel>分级属性</InputLabel>
|
||||
<Select
|
||||
value={styleConfig.property}
|
||||
onChange={(e) => onPropertyChange(e.target.value)}
|
||||
disabled={!selectedRenderLayer}
|
||||
>
|
||||
{availableProperties.map((property) => (
|
||||
<MenuItem key={property.name} value={property.value}>
|
||||
{property.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl variant="standard" fullWidth margin="dense">
|
||||
<InputLabel>分类方法</InputLabel>
|
||||
<Select
|
||||
value={styleConfig.classificationMethod}
|
||||
onChange={(e) => onClassificationMethodChange(e.target.value)}
|
||||
>
|
||||
{CLASSIFICATION_METHODS.map((method) => (
|
||||
<MenuItem key={method.value} value={method.value}>
|
||||
{method.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Box className="mt-3">
|
||||
<Typography gutterBottom>分类数量: {styleConfig.segments}</Typography>
|
||||
<Slider
|
||||
value={styleConfig.segments}
|
||||
onChange={(_, value) => onSegmentsChange(value as number)}
|
||||
min={2}
|
||||
max={10}
|
||||
step={1}
|
||||
marks
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{styleConfig.classificationMethod === "custom_breaks" && (
|
||||
<Box className="mt-3 p-2 bg-gray-50 rounded">
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
手动设置区间阈值(按升序填写,最小值 {">="} 0)
|
||||
</Typography>
|
||||
<Box
|
||||
className="flex flex-col gap-2"
|
||||
sx={{ maxHeight: "160px", overflowY: "auto", paddingTop: "12px" }}
|
||||
>
|
||||
{Array.from({ length: styleConfig.segments }).map((_, index) => (
|
||||
<TextField
|
||||
key={index}
|
||||
label={`阈值 ${index + 1}`}
|
||||
type="number"
|
||||
size="small"
|
||||
slotProps={{ input: { inputProps: { min: 0, step: 0.1 } } }}
|
||||
value={styleConfig.customBreaks?.[index] ?? ""}
|
||||
onChange={(e) => onCustomBreakChange(index, e.target.value)}
|
||||
onBlur={onCustomBreakBlur}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<FormControl variant="standard" fullWidth margin="dense">
|
||||
<InputLabel>
|
||||
<ColorLensIcon className="mr-1" />
|
||||
颜色方案
|
||||
</InputLabel>
|
||||
<Select
|
||||
value={styleConfig.colorType}
|
||||
onChange={(e) => onColorTypeChange(e.target.value)}
|
||||
>
|
||||
{COLOR_TYPE_OPTIONS.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{renderColorSetting()}
|
||||
</FormControl>
|
||||
|
||||
{renderSizeSetting()}
|
||||
|
||||
<Box className="mt-3">
|
||||
<Typography gutterBottom>
|
||||
透明度: {(styleConfig.opacity * 100).toFixed(0)}%
|
||||
</Typography>
|
||||
<Slider
|
||||
value={styleConfig.opacity}
|
||||
onChange={(_, value) =>
|
||||
setStyleConfig((prev) => ({ ...prev, opacity: value as number }))
|
||||
}
|
||||
min={0.1}
|
||||
max={1}
|
||||
step={0.05}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={styleConfig.showId}
|
||||
onChange={(e) =>
|
||||
setStyleConfig((prev) => ({ ...prev, showId: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="显示 ID(缩放 >=15 级时显示)"
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={styleConfig.showLabels}
|
||||
onChange={(e) =>
|
||||
setStyleConfig((prev) => ({ ...prev, showLabels: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="显示属性(缩放 >=15 级时显示)"
|
||||
/>
|
||||
|
||||
<div className="my-3"></div>
|
||||
|
||||
<Box className="flex gap-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={onApply}
|
||||
disabled={!selectedRenderLayer || !styleConfig.property}
|
||||
startIcon={<ApplyIcon />}
|
||||
fullWidth
|
||||
>
|
||||
应用
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onReset}
|
||||
disabled={!selectedRenderLayer}
|
||||
startIcon={<ResetIcon />}
|
||||
fullWidth
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StyleEditorForm;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,7 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
const isCompareMode = data?.isCompareMode ?? false;
|
||||
const junctionText = data?.junctionText ?? "";
|
||||
const pipeText = data?.pipeText ?? "";
|
||||
const setForceStyleAutoApplyVersion = data?.setForceStyleAutoApplyVersion;
|
||||
const { open } = useNotification();
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [playInterval, setPlayInterval] = useState<number>(15000); // 毫秒
|
||||
@@ -657,6 +658,7 @@ const Timeline: React.FC<TimelineProps> = ({
|
||||
});
|
||||
// 清空当天当前时刻及之后的缓存并重新获取数据
|
||||
clearCacheAndRefetch(calculationDate, calculationTime);
|
||||
setForceStyleAutoApplyVersion?.((prev) => prev + 1);
|
||||
} else {
|
||||
open?.({
|
||||
type: "error",
|
||||
|
||||
@@ -14,7 +14,8 @@ import VectorLayer from "ol/layer/Vector";
|
||||
import { Style, Stroke, Fill, Circle } from "ol/style";
|
||||
import Feature from "ol/Feature";
|
||||
import StyleEditorPanel from "./StyleEditorPanel";
|
||||
import { LayerStyleState } from "./StyleEditorPanel";
|
||||
import { createDefaultLayerStyleStates } from "./styleEditorPresets";
|
||||
import { LayerStyleState } from "./styleEditorTypes";
|
||||
import StyleLegend from "./StyleLegend"; // 引入图例组件
|
||||
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
||||
import { useNotification } from "@refinedev/core";
|
||||
@@ -90,81 +91,9 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
||||
});
|
||||
|
||||
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式
|
||||
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([
|
||||
{
|
||||
isActive: false, // 默认不激活,不显示图例
|
||||
layerId: "junctions",
|
||||
layerName: "节点",
|
||||
styleConfig: {
|
||||
property: "pressure",
|
||||
classificationMethod: "custom_breaks",
|
||||
customBreaks: [16, 18, 20, 22, 24, 26],
|
||||
customColors: [
|
||||
"rgba(255, 0, 0, 1)",
|
||||
"rgba(255, 127, 0, 1)",
|
||||
"rgba(255, 215, 0, 1)",
|
||||
"rgba(199, 224, 0, 1)",
|
||||
"rgba(76, 175, 80, 1)",
|
||||
"rgba(0, 158, 115, 1)",
|
||||
],
|
||||
segments: 6,
|
||||
minSize: 4,
|
||||
maxSize: 12,
|
||||
minStrokeWidth: 2,
|
||||
maxStrokeWidth: 8,
|
||||
fixedStrokeWidth: 3,
|
||||
colorType: "rainbow",
|
||||
singlePaletteIndex: 0,
|
||||
gradientPaletteIndex: 0,
|
||||
rainbowPaletteIndex: 0,
|
||||
showLabels: false,
|
||||
showId: false,
|
||||
opacity: 0.9,
|
||||
adjustWidthByProperty: true,
|
||||
},
|
||||
legendConfig: {
|
||||
layerId: "junctions",
|
||||
layerName: "节点",
|
||||
property: "压力", // 暂时为空,等计算后更新
|
||||
colors: [],
|
||||
type: "point",
|
||||
dimensions: [],
|
||||
breaks: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
isActive: false, // 默认不激活,不显示图例
|
||||
layerId: "pipes",
|
||||
layerName: "管道",
|
||||
styleConfig: {
|
||||
property: "flow",
|
||||
classificationMethod: "pretty_breaks",
|
||||
segments: 6,
|
||||
minSize: 4,
|
||||
maxSize: 12,
|
||||
minStrokeWidth: 2,
|
||||
maxStrokeWidth: 8,
|
||||
fixedStrokeWidth: 3,
|
||||
colorType: "gradient",
|
||||
singlePaletteIndex: 0,
|
||||
gradientPaletteIndex: 0,
|
||||
rainbowPaletteIndex: 0,
|
||||
showLabels: false,
|
||||
showId: false,
|
||||
opacity: 0.9,
|
||||
adjustWidthByProperty: true,
|
||||
},
|
||||
legendConfig: {
|
||||
layerId: "pipes",
|
||||
layerName: "管道",
|
||||
property: "流量", // 暂时为空,等计算后更新
|
||||
colors: [],
|
||||
type: "linestring",
|
||||
dimensions: [],
|
||||
breaks: [],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>(
|
||||
() => createDefaultLayerStyleStates()
|
||||
);
|
||||
|
||||
// 计算激活的图例配置
|
||||
const activeLegendConfigs = layerStyleStates
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { LayerStyleState, StyleConfig, DefaultLayerStyleId } from "./styleEditorTypes";
|
||||
|
||||
export const SINGLE_COLOR_PALETTES = [
|
||||
{ color: "rgba(51, 153, 204, 1)" },
|
||||
{ color: "rgba(255, 138, 92, 1)" },
|
||||
{ color: "rgba(204, 51, 51, 1)" },
|
||||
{ color: "rgba(255, 235, 59, 1)" },
|
||||
{ color: "rgba(44, 160, 44, 1)" },
|
||||
{ color: "rgba(227, 119, 194, 1)" },
|
||||
{ color: "rgba(148, 103, 189, 1)" },
|
||||
];
|
||||
|
||||
export const GRADIENT_PALETTES = [
|
||||
{
|
||||
name: "蓝-红",
|
||||
start: "rgba(51, 153, 204, 1)",
|
||||
end: "rgba(204, 51, 51, 1)",
|
||||
},
|
||||
{
|
||||
name: "黄-绿",
|
||||
start: "rgba(255, 235, 59, 1)",
|
||||
end: "rgba(44, 160, 44, 1)",
|
||||
},
|
||||
{
|
||||
name: "粉-紫",
|
||||
start: "rgba(227, 119, 194, 1)",
|
||||
end: "rgba(148, 103, 189, 1)",
|
||||
},
|
||||
];
|
||||
|
||||
export const RAINBOW_PALETTES = [
|
||||
{
|
||||
name: "正向彩虹",
|
||||
colors: [
|
||||
"rgba(255, 0, 0, 1)",
|
||||
"rgba(255, 127, 0, 1)",
|
||||
"rgba(255, 215, 0, 1)",
|
||||
"rgba(199, 224, 0, 1)",
|
||||
"rgba(76, 175, 80, 1)",
|
||||
"rgba(0, 158, 115, 1)",
|
||||
"rgba(0, 188, 212, 1)",
|
||||
"rgba(33, 150, 243, 1)",
|
||||
"rgba(63, 81, 181, 1)",
|
||||
"rgba(142, 68, 173, 1)",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "反向彩虹",
|
||||
colors: [
|
||||
"rgba(142, 68, 173, 1)",
|
||||
"rgba(63, 81, 181, 1)",
|
||||
"rgba(33, 150, 243, 1)",
|
||||
"rgba(0, 188, 212, 1)",
|
||||
"rgba(0, 158, 115, 1)",
|
||||
"rgba(76, 175, 80, 1)",
|
||||
"rgba(199, 224, 0, 1)",
|
||||
"rgba(255, 215, 0, 1)",
|
||||
"rgba(255, 127, 0, 1)",
|
||||
"rgba(255, 0, 0, 1)",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const CLASSIFICATION_METHODS = [
|
||||
{ name: "优雅分段", value: "pretty_breaks" },
|
||||
{ name: "自定义", value: "custom_breaks" },
|
||||
];
|
||||
|
||||
export const COLOR_TYPE_OPTIONS = [
|
||||
{ label: "单一色", value: "single" },
|
||||
{ label: "渐进色", value: "gradient" },
|
||||
{ label: "离散彩虹", value: "rainbow" },
|
||||
{ label: "自定义", value: "custom" },
|
||||
];
|
||||
|
||||
const DEFAULT_LAYER_STYLE_PRESETS: Record<
|
||||
DefaultLayerStyleId,
|
||||
Omit<LayerStyleState, "isActive">
|
||||
> = {
|
||||
junctions: {
|
||||
layerId: "junctions",
|
||||
layerName: "节点",
|
||||
styleConfig: {
|
||||
property: "pressure",
|
||||
classificationMethod: "custom_breaks",
|
||||
customBreaks: [16, 18, 20, 22, 24, 26],
|
||||
customColors: [
|
||||
"rgba(255, 0, 0, 1)",
|
||||
"rgba(255, 127, 0, 1)",
|
||||
"rgba(255, 215, 0, 1)",
|
||||
"rgba(199, 224, 0, 1)",
|
||||
"rgba(76, 175, 80, 1)",
|
||||
"rgba(0, 158, 115, 1)",
|
||||
],
|
||||
segments: 6,
|
||||
minSize: 4,
|
||||
maxSize: 12,
|
||||
minStrokeWidth: 2,
|
||||
maxStrokeWidth: 8,
|
||||
fixedStrokeWidth: 3,
|
||||
colorType: "rainbow",
|
||||
singlePaletteIndex: 0,
|
||||
gradientPaletteIndex: 0,
|
||||
rainbowPaletteIndex: 0,
|
||||
showLabels: true,
|
||||
showId: false,
|
||||
opacity: 0.9,
|
||||
adjustWidthByProperty: true,
|
||||
},
|
||||
legendConfig: {
|
||||
layerId: "junctions",
|
||||
layerName: "节点",
|
||||
property: "压力",
|
||||
colors: [],
|
||||
type: "point",
|
||||
dimensions: [],
|
||||
breaks: [],
|
||||
},
|
||||
},
|
||||
pipes: {
|
||||
layerId: "pipes",
|
||||
layerName: "管道",
|
||||
styleConfig: {
|
||||
property: "velocity",
|
||||
classificationMethod: "custom_breaks",
|
||||
segments: 6,
|
||||
minSize: 4,
|
||||
maxSize: 12,
|
||||
minStrokeWidth: 2,
|
||||
maxStrokeWidth: 8,
|
||||
fixedStrokeWidth: 3,
|
||||
colorType: "gradient",
|
||||
singlePaletteIndex: 0,
|
||||
gradientPaletteIndex: 0,
|
||||
rainbowPaletteIndex: 0,
|
||||
showLabels: true,
|
||||
showId: false,
|
||||
opacity: 0.9,
|
||||
adjustWidthByProperty: true,
|
||||
customBreaks: [0.2, 0.4, 0.6, 0.8, 1.0, 1.2],
|
||||
customColors: [],
|
||||
},
|
||||
legendConfig: {
|
||||
layerId: "pipes",
|
||||
layerName: "管道",
|
||||
property: "流速",
|
||||
colors: [],
|
||||
type: "linestring",
|
||||
dimensions: [],
|
||||
breaks: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const createEmptyStyleConfig = (): StyleConfig => ({
|
||||
property: "",
|
||||
classificationMethod: "pretty_breaks",
|
||||
segments: 5,
|
||||
minSize: 4,
|
||||
maxSize: 12,
|
||||
minStrokeWidth: 2,
|
||||
maxStrokeWidth: 6,
|
||||
fixedStrokeWidth: 3,
|
||||
colorType: "single",
|
||||
singlePaletteIndex: 0,
|
||||
gradientPaletteIndex: 0,
|
||||
rainbowPaletteIndex: 0,
|
||||
showLabels: false,
|
||||
showId: false,
|
||||
opacity: 0.9,
|
||||
adjustWidthByProperty: true,
|
||||
customBreaks: [],
|
||||
customColors: [],
|
||||
});
|
||||
|
||||
export const createDefaultLayerStyleState = (
|
||||
layerId: DefaultLayerStyleId
|
||||
): LayerStyleState => {
|
||||
const preset = DEFAULT_LAYER_STYLE_PRESETS[layerId];
|
||||
return {
|
||||
...preset,
|
||||
styleConfig: {
|
||||
...preset.styleConfig,
|
||||
customBreaks: [...(preset.styleConfig.customBreaks || [])],
|
||||
customColors: [...(preset.styleConfig.customColors || [])],
|
||||
},
|
||||
legendConfig: {
|
||||
...preset.legendConfig,
|
||||
colors: [...preset.legendConfig.colors],
|
||||
dimensions: [...preset.legendConfig.dimensions],
|
||||
breaks: [...preset.legendConfig.breaks],
|
||||
},
|
||||
isActive: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const createDefaultLayerStyleStates = (): LayerStyleState[] => [
|
||||
createDefaultLayerStyleState("junctions"),
|
||||
createDefaultLayerStyleState("pipes"),
|
||||
];
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from "react";
|
||||
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
||||
|
||||
import { LegendStyleConfig } from "./StyleLegend";
|
||||
|
||||
export interface StyleConfig {
|
||||
property: string;
|
||||
classificationMethod: string;
|
||||
segments: number;
|
||||
minSize: number;
|
||||
maxSize: number;
|
||||
minStrokeWidth: number;
|
||||
maxStrokeWidth: number;
|
||||
fixedStrokeWidth: number;
|
||||
colorType: string;
|
||||
singlePaletteIndex: number;
|
||||
gradientPaletteIndex: number;
|
||||
rainbowPaletteIndex: number;
|
||||
showLabels: boolean;
|
||||
showId: boolean;
|
||||
opacity: number;
|
||||
adjustWidthByProperty: boolean;
|
||||
customBreaks?: number[];
|
||||
customColors?: string[];
|
||||
}
|
||||
|
||||
export interface LayerStyleState {
|
||||
layerId: string;
|
||||
layerName: string;
|
||||
styleConfig: StyleConfig;
|
||||
legendConfig: LegendStyleConfig;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export type DefaultLayerStyleId = "junctions" | "pipes";
|
||||
|
||||
export interface StyleEditorPanelProps {
|
||||
layerStyleStates: LayerStyleState[];
|
||||
setLayerStyleStates: React.Dispatch<React.SetStateAction<LayerStyleState[]>>;
|
||||
}
|
||||
|
||||
export interface AvailableProperty {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface StyleEditorFormProps {
|
||||
renderLayers: WebGLVectorTileLayer[];
|
||||
selectedRenderLayer?: WebGLVectorTileLayer;
|
||||
styleConfig: StyleConfig;
|
||||
setStyleConfig: React.Dispatch<React.SetStateAction<StyleConfig>>;
|
||||
availableProperties: AvailableProperty[];
|
||||
onLayerChange: (index: number) => void;
|
||||
onPropertyChange: (property: string) => void;
|
||||
onClassificationMethodChange: (method: string) => void;
|
||||
onSegmentsChange: (segments: number) => void;
|
||||
onCustomBreakChange: (index: number, value: string) => void;
|
||||
onCustomBreakBlur: () => void;
|
||||
onColorTypeChange: (colorType: string) => void;
|
||||
onApply: () => void;
|
||||
onReset: () => void;
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
import { FlatStyleLike } from "ol/style/flat";
|
||||
|
||||
import { calculateClassification } from "@utils/breaks_classification";
|
||||
import { parseColor } from "@utils/parseColor";
|
||||
|
||||
import {
|
||||
GRADIENT_PALETTES,
|
||||
RAINBOW_PALETTES,
|
||||
SINGLE_COLOR_PALETTES,
|
||||
} from "./styleEditorPresets";
|
||||
import { StyleConfig } from "./styleEditorTypes";
|
||||
|
||||
export const rgbaToHex = (rgba: string) => {
|
||||
try {
|
||||
const c = parseColor(rgba);
|
||||
const toHex = (n: number) => {
|
||||
const hex = Math.round(n).toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
};
|
||||
return `#${toHex(c.r)}${toHex(c.g)}${toHex(c.b)}`;
|
||||
} catch {
|
||||
return "#000000";
|
||||
}
|
||||
};
|
||||
|
||||
export const hexToRgba = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
|
||||
result[3],
|
||||
16
|
||||
)}, 1)`
|
||||
: "rgba(0, 0, 0, 1)";
|
||||
};
|
||||
|
||||
export const getDefaultCustomColors = (
|
||||
segments: number,
|
||||
existingColors: string[] = []
|
||||
) => {
|
||||
const nextColors = [...existingColors];
|
||||
const baseColors = RAINBOW_PALETTES[0].colors;
|
||||
|
||||
while (nextColors.length < segments) {
|
||||
nextColors.push(baseColors[nextColors.length % baseColors.length]);
|
||||
}
|
||||
|
||||
return nextColors.slice(0, segments);
|
||||
};
|
||||
|
||||
export const getDefaultCustomBreaks = ({
|
||||
segments,
|
||||
property,
|
||||
layerId,
|
||||
elevationRange,
|
||||
diameterRange,
|
||||
currentJunctionCalData,
|
||||
currentPipeCalData,
|
||||
}: {
|
||||
segments: number;
|
||||
property: string;
|
||||
layerId?: string;
|
||||
elevationRange?: [number, number];
|
||||
diameterRange?: [number, number];
|
||||
currentJunctionCalData?: any[];
|
||||
currentPipeCalData?: any[];
|
||||
}) => {
|
||||
if (!layerId || !property) {
|
||||
return Array.from({ length: segments }, () => 0);
|
||||
}
|
||||
|
||||
let dataArr: number[] = [];
|
||||
|
||||
const isElevation = layerId === "junctions" && property === "elevation";
|
||||
const isDiameter = layerId === "pipes" && property === "diameter";
|
||||
|
||||
if (isElevation && elevationRange) {
|
||||
dataArr = [elevationRange[0], elevationRange[1]];
|
||||
} else if (isDiameter && diameterRange) {
|
||||
dataArr = [diameterRange[0], diameterRange[1]];
|
||||
} else if (layerId === "junctions" && currentJunctionCalData) {
|
||||
dataArr = currentJunctionCalData.map((d: any) => d.value);
|
||||
} else if (layerId === "pipes" && currentPipeCalData) {
|
||||
dataArr = currentPipeCalData.map((d: any) => d.value);
|
||||
}
|
||||
|
||||
if (dataArr.length === 0) {
|
||||
return Array.from({ length: segments }, () => 0);
|
||||
}
|
||||
|
||||
const defaultBreaks = calculateClassification(
|
||||
dataArr,
|
||||
segments,
|
||||
"pretty_breaks"
|
||||
).slice(0, segments);
|
||||
|
||||
while (defaultBreaks.length < segments) {
|
||||
defaultBreaks.push(defaultBreaks[defaultBreaks.length - 1] ?? 0);
|
||||
}
|
||||
|
||||
return defaultBreaks;
|
||||
};
|
||||
|
||||
export const normalizeCustomBreaks = (breaks: number[], desired: number) => {
|
||||
const nextBreaks = [...breaks]
|
||||
.slice(0, desired)
|
||||
.filter((value) => value >= 0)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
while (nextBreaks.length < desired) {
|
||||
nextBreaks.push(nextBreaks[nextBreaks.length - 1] ?? 0);
|
||||
}
|
||||
|
||||
return nextBreaks;
|
||||
};
|
||||
|
||||
export const addBreakExtrema = (breaks: number[], dataValues: number[]) => {
|
||||
const nextBreaks = [...breaks];
|
||||
const minValue = Math.max(
|
||||
dataValues.reduce((min, value) => Math.min(min, value), Infinity),
|
||||
0
|
||||
);
|
||||
const maxValue = dataValues.reduce(
|
||||
(max, value) => Math.max(max, value),
|
||||
-Infinity
|
||||
);
|
||||
|
||||
if (!nextBreaks.includes(minValue)) {
|
||||
nextBreaks.push(minValue);
|
||||
}
|
||||
|
||||
if (!nextBreaks.includes(maxValue)) {
|
||||
nextBreaks.push(maxValue);
|
||||
}
|
||||
|
||||
nextBreaks.sort((a, b) => a - b);
|
||||
return nextBreaks;
|
||||
};
|
||||
|
||||
export const resolveStyleColors = (
|
||||
styleConfig: StyleConfig,
|
||||
breaksLength: number
|
||||
): string[] => {
|
||||
if (styleConfig.colorType === "single") {
|
||||
return Array.from(
|
||||
{ length: breaksLength },
|
||||
() => SINGLE_COLOR_PALETTES[styleConfig.singlePaletteIndex].color
|
||||
);
|
||||
}
|
||||
|
||||
if (styleConfig.colorType === "gradient") {
|
||||
const { start, end } = GRADIENT_PALETTES[styleConfig.gradientPaletteIndex];
|
||||
const startColor = parseColor(start);
|
||||
const endColor = parseColor(end);
|
||||
|
||||
return Array.from({ length: breaksLength }, (_, index) => {
|
||||
const ratio = breaksLength > 1 ? index / (breaksLength - 1) : 1;
|
||||
const r = Math.round(startColor.r + (endColor.r - startColor.r) * ratio);
|
||||
const g = Math.round(startColor.g + (endColor.g - startColor.g) * ratio);
|
||||
const b = Math.round(startColor.b + (endColor.b - startColor.b) * ratio);
|
||||
return `rgba(${r}, ${g}, ${b}, 1)`;
|
||||
});
|
||||
}
|
||||
|
||||
if (styleConfig.colorType === "rainbow") {
|
||||
const baseColors = RAINBOW_PALETTES[styleConfig.rainbowPaletteIndex].colors;
|
||||
return Array.from(
|
||||
{ length: breaksLength },
|
||||
(_, index) => baseColors[index % baseColors.length]
|
||||
);
|
||||
}
|
||||
|
||||
const customColors = styleConfig.customColors || [];
|
||||
const reverseRainbowColors = RAINBOW_PALETTES[1].colors;
|
||||
const result = [...customColors];
|
||||
|
||||
while (result.length < breaksLength) {
|
||||
result.push(
|
||||
reverseRainbowColors[
|
||||
(result.length - customColors.length) % reverseRainbowColors.length
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return result.slice(0, breaksLength);
|
||||
};
|
||||
|
||||
export const getSizePreviewColors = (styleConfig: StyleConfig) => {
|
||||
if (styleConfig.colorType === "single") {
|
||||
const color = SINGLE_COLOR_PALETTES[styleConfig.singlePaletteIndex].color;
|
||||
return [color, color];
|
||||
}
|
||||
|
||||
if (styleConfig.colorType === "gradient") {
|
||||
const { start, end } = GRADIENT_PALETTES[styleConfig.gradientPaletteIndex];
|
||||
return [start, end];
|
||||
}
|
||||
|
||||
if (styleConfig.colorType === "rainbow") {
|
||||
const rainbowColors = RAINBOW_PALETTES[styleConfig.rainbowPaletteIndex].colors;
|
||||
return [rainbowColors[0], rainbowColors[rainbowColors.length - 1]];
|
||||
}
|
||||
|
||||
const customColors = styleConfig.customColors || [];
|
||||
return [
|
||||
customColors[0] || "rgba(0,0,0,1)",
|
||||
customColors[customColors.length - 1] || "rgba(0,0,0,1)",
|
||||
];
|
||||
};
|
||||
|
||||
export const resolveDimensions = ({
|
||||
layerType,
|
||||
styleConfig,
|
||||
breaksLength,
|
||||
}: {
|
||||
layerType: string;
|
||||
styleConfig: StyleConfig;
|
||||
breaksLength: number;
|
||||
}) => {
|
||||
if (layerType === "linestring") {
|
||||
if (styleConfig.adjustWidthByProperty) {
|
||||
return Array.from({ length: breaksLength }, (_, index) => {
|
||||
const ratio = index / (breaksLength - 1);
|
||||
return (
|
||||
styleConfig.minStrokeWidth +
|
||||
(styleConfig.maxStrokeWidth - styleConfig.minStrokeWidth) * ratio
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
{ length: breaksLength },
|
||||
() => styleConfig.fixedStrokeWidth
|
||||
);
|
||||
}
|
||||
|
||||
return Array.from({ length: breaksLength }, (_, index) => {
|
||||
const ratio = index / (breaksLength - 1);
|
||||
return styleConfig.minSize + (styleConfig.maxSize - styleConfig.minSize) * ratio;
|
||||
});
|
||||
};
|
||||
|
||||
export const buildDynamicStyle = ({
|
||||
layerType,
|
||||
styleConfig,
|
||||
breaks,
|
||||
colors,
|
||||
dimensions,
|
||||
}: {
|
||||
layerType: string;
|
||||
styleConfig: StyleConfig;
|
||||
breaks: number[];
|
||||
colors: string[];
|
||||
dimensions: number[];
|
||||
}): FlatStyleLike => {
|
||||
const generateColorConditions = (property: string): any[] => {
|
||||
const conditions: any[] = ["case"];
|
||||
for (let index = 1; index < breaks.length; index++) {
|
||||
if (property === "unit_headloss") {
|
||||
conditions.push([
|
||||
"<=",
|
||||
["/", ["get", "unit_headloss"], ["/", ["get", "length"], 1000]],
|
||||
breaks[index],
|
||||
]);
|
||||
} else {
|
||||
conditions.push(["<=", ["get", property], breaks[index]]);
|
||||
}
|
||||
const colorObj = parseColor(colors[index - 1]);
|
||||
conditions.push(
|
||||
`rgba(${colorObj.r}, ${colorObj.g}, ${colorObj.b}, ${styleConfig.opacity})`
|
||||
);
|
||||
}
|
||||
const defaultColor = parseColor(colors[0]);
|
||||
conditions.push(
|
||||
`rgba(${defaultColor.r}, ${defaultColor.g}, ${defaultColor.b}, ${styleConfig.opacity})`
|
||||
);
|
||||
return conditions;
|
||||
};
|
||||
|
||||
const generateDimensionConditions = (property: string): any[] => {
|
||||
const conditions: any[] = ["case"];
|
||||
for (let index = 0; index < breaks.length; index++) {
|
||||
if (property === "unit_headloss") {
|
||||
conditions.push([
|
||||
"<=",
|
||||
["/", ["get", "headloss"], ["get", "length"]],
|
||||
breaks[index],
|
||||
]);
|
||||
} else {
|
||||
conditions.push(["<=", ["get", property], breaks[index]]);
|
||||
}
|
||||
conditions.push(dimensions[index]);
|
||||
}
|
||||
conditions.push(dimensions[dimensions.length - 1]);
|
||||
return conditions;
|
||||
};
|
||||
|
||||
const generatePointDimensionConditions = (property: string): any[] => {
|
||||
const conditions: any[] = ["case"];
|
||||
for (let index = 0; index < breaks.length; index++) {
|
||||
conditions.push(["<=", ["get", property], breaks[index]]);
|
||||
conditions.push(["interpolate", ["linear"], ["zoom"], 12, 1, 24, dimensions[index]]);
|
||||
}
|
||||
conditions.push(dimensions[dimensions.length - 1]);
|
||||
return conditions;
|
||||
};
|
||||
|
||||
const dynamicStyle: FlatStyleLike = {};
|
||||
|
||||
if (layerType === "linestring") {
|
||||
dynamicStyle["stroke-color"] = generateColorConditions(styleConfig.property);
|
||||
dynamicStyle["stroke-width"] = generateDimensionConditions(styleConfig.property);
|
||||
} else if (layerType === "point") {
|
||||
dynamicStyle["circle-fill-color"] = generateColorConditions(styleConfig.property);
|
||||
dynamicStyle["circle-radius"] = generatePointDimensionConditions(
|
||||
styleConfig.property
|
||||
);
|
||||
dynamicStyle["circle-stroke-color"] = generateColorConditions(styleConfig.property);
|
||||
dynamicStyle["circle-stroke-width"] = 2;
|
||||
}
|
||||
|
||||
return dynamicStyle;
|
||||
};
|
||||
|
||||
export const buildContourDefinitions = ({
|
||||
styleConfig,
|
||||
breaks,
|
||||
colors,
|
||||
}: {
|
||||
styleConfig: StyleConfig;
|
||||
breaks: number[];
|
||||
colors: string[];
|
||||
}) => {
|
||||
const contours = [];
|
||||
for (let index = 0; index < breaks.length - 1; index++) {
|
||||
const colorObj = parseColor(colors[index]);
|
||||
contours.push({
|
||||
threshold: [breaks[index], breaks[index + 1]],
|
||||
color: [
|
||||
colorObj.r,
|
||||
colorObj.g,
|
||||
colorObj.b,
|
||||
Math.round(styleConfig.opacity * 255),
|
||||
],
|
||||
strokeWidth: 0,
|
||||
});
|
||||
}
|
||||
return contours;
|
||||
};
|
||||
@@ -0,0 +1,944 @@
|
||||
import { useNotification } from "@refinedev/core";
|
||||
import { VectorTile } from "ol";
|
||||
import type { Map as OlMap } from "ol";
|
||||
import WebGLVectorTileLayer from "ol/layer/WebGLVectorTile";
|
||||
import VectorTileSource from "ol/source/VectorTile";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { FlatStyleLike } from "ol/style/flat";
|
||||
|
||||
import { config } from "@/config/config";
|
||||
|
||||
import { useData, useMap } from "../MapComponent";
|
||||
import {
|
||||
createDefaultLayerStyleState,
|
||||
createEmptyStyleConfig,
|
||||
} from "./styleEditorPresets";
|
||||
import {
|
||||
addBreakExtrema,
|
||||
buildContourDefinitions,
|
||||
buildDynamicStyle,
|
||||
getDefaultCustomBreaks,
|
||||
getDefaultCustomColors,
|
||||
normalizeCustomBreaks,
|
||||
resolveDimensions,
|
||||
resolveStyleColors,
|
||||
} from "./styleEditorUtils";
|
||||
import {
|
||||
AvailableProperty,
|
||||
LayerStyleState,
|
||||
StyleEditorPanelProps,
|
||||
} from "./styleEditorTypes";
|
||||
import { LegendStyleConfig } from "./StyleLegend";
|
||||
import { calculateClassification } from "@utils/breaks_classification";
|
||||
|
||||
const UNIT_HEADLOSS_RANGE: [number, number] = [0, 5];
|
||||
|
||||
export const useStyleEditor = ({
|
||||
layerStyleStates,
|
||||
setLayerStyleStates,
|
||||
}: StyleEditorPanelProps) => {
|
||||
const map = useMap();
|
||||
const data = useData();
|
||||
const { open } = useNotification();
|
||||
|
||||
const currentJunctionCalData = data?.currentJunctionCalData;
|
||||
const currentPipeCalData = data?.currentPipeCalData;
|
||||
const compareJunctionCalData = data?.compareJunctionCalData;
|
||||
const comparePipeCalData = data?.comparePipeCalData;
|
||||
const compareMap = data?.compareMap;
|
||||
const activeMaps = useMemo<OlMap[]>(
|
||||
() => (data?.maps?.length ? data.maps : map ? [map] : []),
|
||||
[data?.maps, map]
|
||||
);
|
||||
const junctionText = data?.junctionText ?? "";
|
||||
const pipeText = data?.pipeText ?? "";
|
||||
const setShowJunctionTextLayer = data?.setShowJunctionTextLayer;
|
||||
const setShowPipeTextLayer = data?.setShowPipeTextLayer;
|
||||
const setShowJunctionId = data?.setShowJunctionId;
|
||||
const setShowPipeId = data?.setShowPipeId;
|
||||
const setContourLayerAvailable = data?.setContourLayerAvailable;
|
||||
const setWaterflowLayerAvailable = data?.setWaterflowLayerAvailable;
|
||||
const setJunctionText = data?.setJunctionText;
|
||||
const setPipeText = data?.setPipeText;
|
||||
const setContours = data?.setContours;
|
||||
const diameterRange = data?.diameterRange;
|
||||
const elevationRange = data?.elevationRange;
|
||||
const forceStyleAutoApplyVersion = data?.forceStyleAutoApplyVersion ?? 0;
|
||||
|
||||
const [applyJunctionStyle, setApplyJunctionStyle] = useState(false);
|
||||
const [applyPipeStyle, setApplyPipeStyle] = useState(false);
|
||||
const [styleUpdateTrigger, setStyleUpdateTrigger] = useState(0);
|
||||
const prevStyleUpdateTriggerRef = useRef(0);
|
||||
const lastForceStyleAutoApplyVersionRef = useRef(0);
|
||||
|
||||
const [renderLayers, setRenderLayers] = useState<WebGLVectorTileLayer[]>([]);
|
||||
const [selectedRenderLayer, setSelectedRenderLayer] =
|
||||
useState<WebGLVectorTileLayer>();
|
||||
const [styleConfig, setStyleConfig] = useState(createEmptyStyleConfig);
|
||||
const latestLayerStyleStatesRef = useRef(layerStyleStates);
|
||||
|
||||
const tileLoadListenersRef = useRef<
|
||||
Map<string, { source: VectorTileSource; listener: (event: any) => void }>
|
||||
>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
latestLayerStyleStatesRef.current = layerStyleStates;
|
||||
}, [layerStyleStates]);
|
||||
|
||||
const getRenderLayersById = useCallback(
|
||||
(layerId: string) =>
|
||||
activeMaps.flatMap((targetMap) =>
|
||||
targetMap
|
||||
.getAllLayers()
|
||||
.filter((layer) => layer.get("value") === layerId)
|
||||
.filter(
|
||||
(layer): layer is WebGLVectorTileLayer =>
|
||||
layer instanceof WebGLVectorTileLayer
|
||||
)
|
||||
),
|
||||
[activeMaps]
|
||||
);
|
||||
|
||||
const getMapKey = useCallback((targetMap: OlMap, layerId: string) => {
|
||||
const mapUid = (targetMap as unknown as { ol_uid?: string }).ol_uid || "map";
|
||||
return `${mapUid}:${layerId}`;
|
||||
}, []);
|
||||
|
||||
const getDataForMap = useCallback(
|
||||
(targetMap: OlMap, layerId: string) => {
|
||||
if (layerId === "junctions") {
|
||||
return targetMap === compareMap
|
||||
? compareJunctionCalData || []
|
||||
: currentJunctionCalData || [];
|
||||
}
|
||||
if (layerId === "pipes") {
|
||||
return targetMap === compareMap
|
||||
? comparePipeCalData || []
|
||||
: currentPipeCalData || [];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
[
|
||||
compareJunctionCalData,
|
||||
compareMap,
|
||||
comparePipeCalData,
|
||||
currentJunctionCalData,
|
||||
currentPipeCalData,
|
||||
]
|
||||
);
|
||||
|
||||
const availableProperties = useMemo<AvailableProperty[]>(() => {
|
||||
if (!selectedRenderLayer) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (selectedRenderLayer.get("properties") || []) as AvailableProperty[];
|
||||
}, [selectedRenderLayer]);
|
||||
|
||||
const getBreakDefaults = useCallback(
|
||||
(segments: number, property: string, layer = selectedRenderLayer) =>
|
||||
getDefaultCustomBreaks({
|
||||
segments,
|
||||
property,
|
||||
layerId: layer?.get("value"),
|
||||
elevationRange,
|
||||
diameterRange,
|
||||
currentJunctionCalData,
|
||||
currentPipeCalData,
|
||||
}),
|
||||
[
|
||||
currentJunctionCalData,
|
||||
currentPipeCalData,
|
||||
diameterRange,
|
||||
elevationRange,
|
||||
selectedRenderLayer,
|
||||
]
|
||||
);
|
||||
|
||||
const saveLayerStyle = useCallback(
|
||||
(
|
||||
layerId?: string,
|
||||
newLegendConfig?: LegendStyleConfig,
|
||||
overrideStyleConfig = styleConfig
|
||||
) => {
|
||||
if (!overrideStyleConfig.property || !layerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layerName =
|
||||
newLegendConfig?.layerName ||
|
||||
selectedRenderLayer?.get("name") ||
|
||||
`图层${layerId}`;
|
||||
const property = availableProperties.find(
|
||||
(item) => item.value === overrideStyleConfig.property
|
||||
);
|
||||
|
||||
const legendConfig: LegendStyleConfig = newLegendConfig || {
|
||||
layerId,
|
||||
layerName,
|
||||
property: property?.name || overrideStyleConfig.property,
|
||||
colors: [],
|
||||
type: selectedRenderLayer?.get("type") || "point",
|
||||
dimensions: [],
|
||||
breaks: [],
|
||||
};
|
||||
|
||||
const newStyleState: LayerStyleState = {
|
||||
layerId,
|
||||
layerName,
|
||||
styleConfig: { ...overrideStyleConfig },
|
||||
legendConfig: { ...legendConfig },
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
setLayerStyleStates((prev) => {
|
||||
const existingIndex = prev.findIndex((state) => state.layerId === layerId);
|
||||
if (existingIndex !== -1) {
|
||||
const existingState = prev[existingIndex];
|
||||
if (
|
||||
JSON.stringify(existingState.styleConfig) ===
|
||||
JSON.stringify(newStyleState.styleConfig) &&
|
||||
JSON.stringify(existingState.legendConfig) ===
|
||||
JSON.stringify(newStyleState.legendConfig) &&
|
||||
existingState.layerName === newStyleState.layerName &&
|
||||
existingState.isActive === newStyleState.isActive
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
const updated = [...prev];
|
||||
updated[existingIndex] = newStyleState;
|
||||
return updated;
|
||||
}
|
||||
return [...prev, newStyleState];
|
||||
});
|
||||
},
|
||||
[availableProperties, selectedRenderLayer, setLayerStyleStates, styleConfig]
|
||||
);
|
||||
|
||||
const applyContourLayerStyle = useCallback(
|
||||
(layerStyleConfig: LayerStyleState, breaks?: number[]) => {
|
||||
if (!breaks || breaks.length === 0 || !setContours) {
|
||||
return;
|
||||
}
|
||||
|
||||
const colors = resolveStyleColors(layerStyleConfig.styleConfig, breaks.length);
|
||||
setContours(
|
||||
buildContourDefinitions({
|
||||
styleConfig: layerStyleConfig.styleConfig,
|
||||
breaks,
|
||||
colors,
|
||||
})
|
||||
);
|
||||
},
|
||||
[setContours]
|
||||
);
|
||||
|
||||
const applyLayerStyle = useCallback(
|
||||
(layerStyleConfig: LayerStyleState, breaks?: number[]) => {
|
||||
if (!breaks || breaks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStyleConfig = layerStyleConfig.styleConfig;
|
||||
const targetLayers = getRenderLayersById(layerStyleConfig.layerId);
|
||||
const renderLayer = targetLayers[0];
|
||||
if (!renderLayer || !nextStyleConfig.property) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layerType = renderLayer.get("type") as string;
|
||||
const colors = resolveStyleColors(nextStyleConfig, breaks.length);
|
||||
const dimensions = resolveDimensions({
|
||||
layerType,
|
||||
styleConfig: nextStyleConfig,
|
||||
breaksLength: breaks.length,
|
||||
});
|
||||
const dynamicStyle = buildDynamicStyle({
|
||||
layerType,
|
||||
styleConfig: nextStyleConfig,
|
||||
breaks,
|
||||
colors,
|
||||
dimensions,
|
||||
});
|
||||
|
||||
targetLayers.forEach((targetLayer) => {
|
||||
targetLayer.setStyle(dynamicStyle);
|
||||
});
|
||||
|
||||
const layerId = renderLayer.get("value");
|
||||
const initLayerStyleState = latestLayerStyleStatesRef.current.find(
|
||||
(state) => state.layerId === layerId
|
||||
);
|
||||
const legendConfig: LegendStyleConfig = {
|
||||
layerName: initLayerStyleState?.layerName || `图层${layerId}`,
|
||||
layerId,
|
||||
property: initLayerStyleState?.legendConfig.property || "",
|
||||
colors,
|
||||
type: layerType,
|
||||
dimensions,
|
||||
breaks,
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
saveLayerStyle(layerId, legendConfig, nextStyleConfig);
|
||||
}, 100);
|
||||
},
|
||||
[getRenderLayersById, saveLayerStyle]
|
||||
);
|
||||
|
||||
const applyClassificationStyle = useCallback(
|
||||
(layerType: "junctions" | "pipes", fallbackStyleConfig?: LayerStyleState["styleConfig"]) => {
|
||||
const layerStyleState = latestLayerStyleStatesRef.current.find(
|
||||
(state) => state.layerId === layerType
|
||||
);
|
||||
const effectiveStyleConfig = layerStyleState?.styleConfig || fallbackStyleConfig;
|
||||
|
||||
if (!effectiveStyleConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isElevation =
|
||||
layerType === "junctions" && effectiveStyleConfig.property === "elevation";
|
||||
const isDiameter =
|
||||
layerType === "pipes" && effectiveStyleConfig.property === "diameter";
|
||||
const isUnitHeadloss =
|
||||
layerType === "pipes" && effectiveStyleConfig.property === "unit_headloss";
|
||||
|
||||
const dataValues =
|
||||
layerType === "junctions"
|
||||
? isElevation && elevationRange
|
||||
? [elevationRange[0], elevationRange[1]]
|
||||
: currentJunctionCalData?.map((item: any) => item.value) || []
|
||||
: isDiameter && diameterRange
|
||||
? [diameterRange[0], diameterRange[1]]
|
||||
: isUnitHeadloss
|
||||
? [UNIT_HEADLOSS_RANGE[0], UNIT_HEADLOSS_RANGE[1]]
|
||||
: currentPipeCalData?.map((item: any) => item.value) || [];
|
||||
|
||||
const canApply =
|
||||
layerType === "junctions"
|
||||
? dataValues.length > 0
|
||||
: dataValues.length > 0 || isUnitHeadloss;
|
||||
|
||||
if (!canApply || dataValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const segments = effectiveStyleConfig.segments ?? 5;
|
||||
let breaks =
|
||||
effectiveStyleConfig.classificationMethod === "custom_breaks"
|
||||
? normalizeCustomBreaks(effectiveStyleConfig.customBreaks || [], segments)
|
||||
: calculateClassification(
|
||||
dataValues,
|
||||
segments,
|
||||
effectiveStyleConfig.classificationMethod
|
||||
);
|
||||
|
||||
if (breaks.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
breaks = addBreakExtrema(breaks, dataValues);
|
||||
|
||||
const styleStateToApply =
|
||||
layerStyleState ||
|
||||
({
|
||||
layerId: layerType,
|
||||
layerName: layerType === "junctions" ? "节点" : "管道",
|
||||
styleConfig: effectiveStyleConfig,
|
||||
legendConfig: {
|
||||
layerId: layerType,
|
||||
layerName: layerType === "junctions" ? "节点" : "管道",
|
||||
property: effectiveStyleConfig.property,
|
||||
colors: [],
|
||||
type: layerType === "junctions" ? "point" : "linestring",
|
||||
dimensions: [],
|
||||
breaks: [],
|
||||
},
|
||||
isActive: true,
|
||||
} as LayerStyleState);
|
||||
|
||||
applyLayerStyle(styleStateToApply, breaks);
|
||||
if (layerType === "junctions") {
|
||||
applyContourLayerStyle(styleStateToApply, breaks);
|
||||
}
|
||||
},
|
||||
[
|
||||
applyContourLayerStyle,
|
||||
applyLayerStyle,
|
||||
currentJunctionCalData,
|
||||
currentPipeCalData,
|
||||
diameterRange,
|
||||
elevationRange,
|
||||
]
|
||||
);
|
||||
|
||||
const updateVectorTileSource = useCallback(
|
||||
(targetMap: OlMap, layerId: string, property: string, records: any[]) => {
|
||||
const vectorTileSources = targetMap
|
||||
.getAllLayers()
|
||||
.filter((layer) => layer.get("value") === layerId)
|
||||
.map((layer) => layer.getSource() as VectorTileSource)
|
||||
.filter((source) => source);
|
||||
|
||||
if (!vectorTileSources.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataMap = new Map<string, number>();
|
||||
records.forEach((record: any) => {
|
||||
dataMap.set(record.ID, record.value || 0);
|
||||
});
|
||||
|
||||
vectorTileSources.forEach((vectorTileSource) => {
|
||||
const sourceTiles = (vectorTileSource as any).sourceTiles_;
|
||||
Object.values(sourceTiles).forEach((vectorTile: any) => {
|
||||
const renderFeatures = vectorTile.getFeatures();
|
||||
if (!renderFeatures || renderFeatures.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderFeatures.forEach((renderFeature: any) => {
|
||||
const featureId = renderFeature.get("id");
|
||||
const value = dataMap.get(featureId);
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderFeature.properties_[property] =
|
||||
property === "flow" ? Math.abs(value) : value;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const attachVectorTileSourceLoadedEvent = useCallback(
|
||||
(targetMap: OlMap, layerId: string, property: string, records: any[]) => {
|
||||
const vectorTileSource = targetMap
|
||||
.getAllLayers()
|
||||
.filter((layer) => layer.get("value") === layerId)
|
||||
.map((layer) => layer.getSource() as VectorTileSource)
|
||||
.filter((source) => source)[0];
|
||||
|
||||
if (!vectorTileSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataMap = new Map<string, number>();
|
||||
records.forEach((record: any) => {
|
||||
dataMap.set(record.ID, record.value || 0);
|
||||
});
|
||||
|
||||
const listener = (event: any) => {
|
||||
try {
|
||||
if (!(event.tile instanceof VectorTile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderFeatures = event.tile.getFeatures();
|
||||
if (!renderFeatures || renderFeatures.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderFeatures.forEach((renderFeature: any) => {
|
||||
const featureId = renderFeature.get("id");
|
||||
const value = dataMap.get(featureId);
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderFeature.properties_[property] =
|
||||
property === "flow" ? Math.abs(value) : value;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error processing tile load event:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const listenerKey = getMapKey(targetMap, layerId);
|
||||
vectorTileSource.on("tileloadend", listener);
|
||||
tileLoadListenersRef.current.set(listenerKey, {
|
||||
source: vectorTileSource,
|
||||
listener,
|
||||
});
|
||||
},
|
||||
[getMapKey]
|
||||
);
|
||||
|
||||
const removeVectorTileSourceLoadedEvent = useCallback(
|
||||
(targetMap: OlMap, layerId: string) => {
|
||||
const listenerKey = getMapKey(targetMap, layerId);
|
||||
const listenerState = tileLoadListenersRef.current.get(listenerKey);
|
||||
if (listenerState) {
|
||||
listenerState.source.un("tileloadend", listenerState.listener);
|
||||
tileLoadListenersRef.current.delete(listenerKey);
|
||||
}
|
||||
},
|
||||
[getMapKey]
|
||||
);
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
if (!selectedRenderLayer || !styleConfig.property) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layerId = selectedRenderLayer.get("value");
|
||||
const property = styleConfig.property;
|
||||
|
||||
if (styleConfig.classificationMethod === "custom_breaks") {
|
||||
const expected = styleConfig.segments;
|
||||
const custom = styleConfig.customBreaks || [];
|
||||
|
||||
if (
|
||||
custom.length !== expected ||
|
||||
custom.some((value) => value === undefined || value === null || isNaN(value))
|
||||
) {
|
||||
open?.({
|
||||
type: "error",
|
||||
message: `请设置 ${expected} 个有效的自定义阈值(数字)`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (custom.some((value) => value < 0)) {
|
||||
open?.({ type: "error", message: "自定义阈值必须大于等于 0" });
|
||||
return;
|
||||
}
|
||||
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
customBreaks: [...(prev.customBreaks || [])]
|
||||
.slice(0, expected)
|
||||
.sort((a, b) => a - b),
|
||||
}));
|
||||
}
|
||||
|
||||
if (layerId === "junctions") {
|
||||
setJunctionText?.(property);
|
||||
setShowJunctionTextLayer?.(styleConfig.showLabels);
|
||||
setShowJunctionId?.(styleConfig.showId);
|
||||
setApplyJunctionStyle(true);
|
||||
if (property === "pressure") {
|
||||
setContourLayerAvailable?.(true);
|
||||
}
|
||||
saveLayerStyle(layerId);
|
||||
open?.({
|
||||
type: "success",
|
||||
message: "节点图层样式设置成功,等待数据更新。",
|
||||
});
|
||||
}
|
||||
|
||||
if (layerId === "pipes") {
|
||||
setPipeText?.(property);
|
||||
setShowPipeTextLayer?.(styleConfig.showLabels);
|
||||
setShowPipeId?.(styleConfig.showId);
|
||||
setApplyPipeStyle(true);
|
||||
setWaterflowLayerAvailable?.(true);
|
||||
saveLayerStyle(layerId);
|
||||
open?.({
|
||||
type: "success",
|
||||
message: "管道图层样式设置成功,等待数据更新。",
|
||||
});
|
||||
}
|
||||
|
||||
setStyleUpdateTrigger((prev) => prev + 1);
|
||||
}, [
|
||||
open,
|
||||
saveLayerStyle,
|
||||
selectedRenderLayer,
|
||||
setContourLayerAvailable,
|
||||
setJunctionText,
|
||||
setPipeText,
|
||||
setShowJunctionId,
|
||||
setShowJunctionTextLayer,
|
||||
setShowPipeId,
|
||||
setShowPipeTextLayer,
|
||||
setWaterflowLayerAvailable,
|
||||
styleConfig,
|
||||
]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
if (!selectedRenderLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE;
|
||||
const layerId = selectedRenderLayer.get("value");
|
||||
|
||||
getRenderLayersById(layerId).forEach((targetLayer) => {
|
||||
targetLayer.setStyle(defaultFlatStyle);
|
||||
});
|
||||
|
||||
setLayerStyleStates((prev) => prev.filter((state) => state.layerId !== layerId));
|
||||
|
||||
if (layerId === "junctions") {
|
||||
setApplyJunctionStyle(false);
|
||||
setShowJunctionTextLayer?.(false);
|
||||
setShowJunctionId?.(false);
|
||||
setJunctionText?.("");
|
||||
setContours?.([]);
|
||||
setContourLayerAvailable?.(false);
|
||||
} else if (layerId === "pipes") {
|
||||
setApplyPipeStyle(false);
|
||||
setShowPipeTextLayer?.(false);
|
||||
setShowPipeId?.(false);
|
||||
setPipeText?.("");
|
||||
setWaterflowLayerAvailable?.(false);
|
||||
}
|
||||
}, [
|
||||
getRenderLayersById,
|
||||
selectedRenderLayer,
|
||||
setContourLayerAvailable,
|
||||
setContours,
|
||||
setJunctionText,
|
||||
setLayerStyleStates,
|
||||
setPipeText,
|
||||
setShowJunctionId,
|
||||
setShowJunctionTextLayer,
|
||||
setShowPipeId,
|
||||
setShowPipeTextLayer,
|
||||
setWaterflowLayerAvailable,
|
||||
]);
|
||||
|
||||
const handleLayerChange = useCallback(
|
||||
(index: number) => {
|
||||
const newLayer = index >= 0 ? renderLayers[index] : undefined;
|
||||
setSelectedRenderLayer(newLayer);
|
||||
|
||||
if (!newLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layerId = newLayer.get("value");
|
||||
const cachedStyleState = layerStyleStates.find((state) => state.layerId === layerId);
|
||||
|
||||
if (cachedStyleState) {
|
||||
setStyleConfig(cachedStyleState.styleConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
property: "",
|
||||
customBreaks:
|
||||
prev.classificationMethod === "custom_breaks"
|
||||
? getBreakDefaults(prev.segments, "", newLayer)
|
||||
: prev.customBreaks,
|
||||
customColors: getDefaultCustomColors(prev.segments, prev.customColors),
|
||||
}));
|
||||
},
|
||||
[getBreakDefaults, layerStyleStates, renderLayers]
|
||||
);
|
||||
|
||||
const handlePropertyChange = useCallback(
|
||||
(property: string) => {
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
property,
|
||||
customBreaks:
|
||||
prev.classificationMethod === "custom_breaks"
|
||||
? getBreakDefaults(prev.segments, property)
|
||||
: prev.customBreaks,
|
||||
}));
|
||||
},
|
||||
[getBreakDefaults]
|
||||
);
|
||||
|
||||
const handleClassificationMethodChange = useCallback(
|
||||
(classificationMethod: string) => {
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
classificationMethod,
|
||||
customBreaks:
|
||||
classificationMethod === "custom_breaks"
|
||||
? getBreakDefaults(prev.segments, prev.property)
|
||||
: prev.customBreaks,
|
||||
}));
|
||||
},
|
||||
[getBreakDefaults]
|
||||
);
|
||||
|
||||
const handleSegmentsChange = useCallback(
|
||||
(segments: number) => {
|
||||
setStyleConfig((prev) => {
|
||||
const newCustomColors = [...(prev.customColors || [])];
|
||||
return {
|
||||
...prev,
|
||||
segments,
|
||||
customBreaks:
|
||||
prev.classificationMethod === "custom_breaks"
|
||||
? getBreakDefaults(segments, prev.property)
|
||||
: prev.customBreaks,
|
||||
customColors: getDefaultCustomColors(segments, newCustomColors),
|
||||
};
|
||||
});
|
||||
},
|
||||
[getBreakDefaults]
|
||||
);
|
||||
|
||||
const handleCustomBreakChange = useCallback(
|
||||
(index: number, value: string) => {
|
||||
const nextValue = parseFloat(value);
|
||||
setStyleConfig((prev) => {
|
||||
const nextBreaks = [...(prev.customBreaks || [])];
|
||||
while (nextBreaks.length < prev.segments) {
|
||||
nextBreaks.push(0);
|
||||
}
|
||||
nextBreaks[index] = isNaN(nextValue) ? 0 : Math.max(0, nextValue);
|
||||
return { ...prev, customBreaks: nextBreaks };
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCustomBreakBlur = useCallback(() => {
|
||||
setStyleConfig((prev) => ({
|
||||
...prev,
|
||||
customBreaks: [...(prev.customBreaks || [])]
|
||||
.slice(0, prev.segments + 1)
|
||||
.sort((a, b) => a - b),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleColorTypeChange = useCallback((colorType: string) => {
|
||||
setStyleConfig((prev) => {
|
||||
let customColors = prev.customColors;
|
||||
if (colorType === "custom" && (!customColors || customColors.length === 0)) {
|
||||
customColors = getDefaultCustomColors(prev.segments, []);
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
colorType,
|
||||
adjustWidthByProperty: colorType === "single" ? true : prev.adjustWidthByProperty,
|
||||
customColors,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
forceStyleAutoApplyVersion <= 0 ||
|
||||
forceStyleAutoApplyVersion === lastForceStyleAutoApplyVersionRef.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastForceStyleAutoApplyVersionRef.current = forceStyleAutoApplyVersion;
|
||||
|
||||
const defaultJunctionStyleState = {
|
||||
...createDefaultLayerStyleState("junctions"),
|
||||
isActive: true,
|
||||
};
|
||||
const defaultPipeStyleState = {
|
||||
...createDefaultLayerStyleState("pipes"),
|
||||
isActive: true,
|
||||
};
|
||||
|
||||
setLayerStyleStates((prev) => {
|
||||
const nextStates = [...prev];
|
||||
[defaultJunctionStyleState, defaultPipeStyleState].forEach((defaultState) => {
|
||||
const index = nextStates.findIndex((state) => state.layerId === defaultState.layerId);
|
||||
if (index === -1) {
|
||||
nextStates.push(defaultState);
|
||||
} else {
|
||||
nextStates[index] = defaultState;
|
||||
}
|
||||
});
|
||||
return nextStates;
|
||||
});
|
||||
|
||||
setJunctionText?.(defaultJunctionStyleState.styleConfig.property);
|
||||
setPipeText?.(defaultPipeStyleState.styleConfig.property);
|
||||
setShowJunctionTextLayer?.(defaultJunctionStyleState.styleConfig.showLabels);
|
||||
setShowPipeTextLayer?.(defaultPipeStyleState.styleConfig.showLabels);
|
||||
setShowJunctionId?.(defaultJunctionStyleState.styleConfig.showId);
|
||||
setShowPipeId?.(defaultPipeStyleState.styleConfig.showId);
|
||||
setContourLayerAvailable?.(
|
||||
defaultJunctionStyleState.styleConfig.property === "pressure"
|
||||
);
|
||||
setWaterflowLayerAvailable?.(
|
||||
defaultPipeStyleState.styleConfig.property === "flow"
|
||||
);
|
||||
setApplyJunctionStyle(true);
|
||||
setApplyPipeStyle(true);
|
||||
|
||||
const selectedLayerId = selectedRenderLayer?.get("value");
|
||||
if (selectedLayerId === "junctions") {
|
||||
setStyleConfig(defaultJunctionStyleState.styleConfig);
|
||||
} else if (selectedLayerId === "pipes") {
|
||||
setStyleConfig(defaultPipeStyleState.styleConfig);
|
||||
}
|
||||
}, [
|
||||
forceStyleAutoApplyVersion,
|
||||
selectedRenderLayer,
|
||||
setContourLayerAvailable,
|
||||
setJunctionText,
|
||||
setLayerStyleStates,
|
||||
setPipeText,
|
||||
setShowJunctionId,
|
||||
setShowJunctionTextLayer,
|
||||
setShowPipeId,
|
||||
setShowPipeTextLayer,
|
||||
setWaterflowLayerAvailable,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const isUserTrigger = styleUpdateTrigger !== prevStyleUpdateTriggerRef.current;
|
||||
prevStyleUpdateTriggerRef.current = styleUpdateTrigger;
|
||||
|
||||
const updateJunctionStyle = () => {
|
||||
const junctionStyleState = latestLayerStyleStatesRef.current.find(
|
||||
(state) => state.layerId === "junctions"
|
||||
);
|
||||
const isElevation =
|
||||
junctionStyleState?.styleConfig.property === "elevation";
|
||||
|
||||
applyClassificationStyle("junctions", junctionStyleState?.styleConfig);
|
||||
|
||||
if (isElevation) {
|
||||
activeMaps.forEach((targetMap) => {
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
activeMaps.forEach((targetMap) => {
|
||||
const targetData = getDataForMap(targetMap, "junctions");
|
||||
if (!targetData || targetData.length === 0) {
|
||||
return;
|
||||
}
|
||||
updateVectorTileSource(targetMap, "junctions", junctionText, targetData);
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||
attachVectorTileSourceLoadedEvent(
|
||||
targetMap,
|
||||
"junctions",
|
||||
junctionText,
|
||||
targetData
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const updatePipeStyle = () => {
|
||||
const pipeStyleState = latestLayerStyleStatesRef.current.find(
|
||||
(state) => state.layerId === "pipes"
|
||||
);
|
||||
const isDiameter = pipeStyleState?.styleConfig.property === "diameter";
|
||||
|
||||
applyClassificationStyle("pipes", pipeStyleState?.styleConfig);
|
||||
|
||||
if (isDiameter) {
|
||||
activeMaps.forEach((targetMap) => {
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
activeMaps.forEach((targetMap) => {
|
||||
const targetData = getDataForMap(targetMap, "pipes");
|
||||
if (!targetData || targetData.length === 0) {
|
||||
return;
|
||||
}
|
||||
updateVectorTileSource(targetMap, "pipes", pipeText, targetData);
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||
attachVectorTileSourceLoadedEvent(targetMap, "pipes", pipeText, targetData);
|
||||
});
|
||||
};
|
||||
|
||||
if (isUserTrigger) {
|
||||
if (selectedRenderLayer?.get("value") === "junctions") {
|
||||
updateJunctionStyle();
|
||||
} else if (selectedRenderLayer?.get("value") === "pipes") {
|
||||
updatePipeStyle();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isElevation = junctionText === "elevation";
|
||||
const isDiameter = pipeText === "diameter";
|
||||
|
||||
if (
|
||||
applyJunctionStyle &&
|
||||
((currentJunctionCalData && currentJunctionCalData.length > 0) || isElevation)
|
||||
) {
|
||||
updateJunctionStyle();
|
||||
}
|
||||
|
||||
if (
|
||||
applyPipeStyle &&
|
||||
((currentPipeCalData && currentPipeCalData.length > 0) || isDiameter)
|
||||
) {
|
||||
updatePipeStyle();
|
||||
}
|
||||
|
||||
if (!applyJunctionStyle) {
|
||||
activeMaps.forEach((targetMap) => {
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||
});
|
||||
}
|
||||
|
||||
if (!applyPipeStyle) {
|
||||
activeMaps.forEach((targetMap) => {
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||
});
|
||||
}
|
||||
// This effect is intentionally driven by explicit style triggers and data snapshots.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
styleUpdateTrigger,
|
||||
applyJunctionStyle,
|
||||
applyPipeStyle,
|
||||
currentJunctionCalData,
|
||||
currentPipeCalData,
|
||||
compareJunctionCalData,
|
||||
comparePipeCalData,
|
||||
activeMaps,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
activeMaps.forEach((targetMap) => {
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "junctions");
|
||||
removeVectorTileSourceLoadedEvent(targetMap, "pipes");
|
||||
});
|
||||
};
|
||||
}, [activeMaps, removeVectorTileSourceLoadedEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateVisibleLayers = () => {
|
||||
const layers = map.getAllLayers();
|
||||
const webGLVectorTileLayers = layers.filter(
|
||||
(layer) =>
|
||||
layer.get("value") === "junctions" || layer.get("value") === "pipes"
|
||||
) as WebGLVectorTileLayer[];
|
||||
|
||||
setRenderLayers(webGLVectorTileLayers);
|
||||
};
|
||||
|
||||
updateVisibleLayers();
|
||||
}, [map]);
|
||||
|
||||
return {
|
||||
isReady: Boolean(data),
|
||||
renderLayers,
|
||||
selectedRenderLayer,
|
||||
styleConfig,
|
||||
setStyleConfig,
|
||||
availableProperties,
|
||||
handleLayerChange,
|
||||
handlePropertyChange,
|
||||
handleClassificationMethodChange,
|
||||
handleSegmentsChange,
|
||||
handleCustomBreakChange,
|
||||
handleCustomBreakBlur,
|
||||
handleColorTypeChange,
|
||||
handleApply,
|
||||
handleReset,
|
||||
};
|
||||
};
|
||||
@@ -85,6 +85,8 @@ interface DataContextType {
|
||||
maps?: OlMap[];
|
||||
diameterRange?: [number, number];
|
||||
elevationRange?: [number, number];
|
||||
forceStyleAutoApplyVersion?: number;
|
||||
setForceStyleAutoApplyVersion?: React.Dispatch<React.SetStateAction<number>>;
|
||||
}
|
||||
|
||||
// 跨组件传递
|
||||
@@ -184,7 +186,7 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
const [showPipeId, setShowPipeId] = useState(false); // 控制管道ID显示
|
||||
const [showContourLayer, setShowContourLayer] = useState(false); // 控制等高线图层显示
|
||||
const [junctionText, setJunctionText] = useState("pressure");
|
||||
const [pipeText, setPipeText] = useState("flow");
|
||||
const [pipeText, setPipeText] = useState("velocity");
|
||||
const [contours, setContours] = useState<any[]>([]);
|
||||
const flowAnimation = useRef(false); // 添加动画控制标志
|
||||
const [isContourLayerAvailable, setContourLayerAvailable] = useState(false); // 控制等高线图层显示
|
||||
@@ -263,6 +265,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
const [elevationRange, setElevationRange] = useState<
|
||||
[number, number] | undefined
|
||||
>();
|
||||
const [forceStyleAutoApplyVersion, setForceStyleAutoApplyVersion] =
|
||||
useState(0);
|
||||
|
||||
const toggleCompareMode = useCallback(() => {
|
||||
setCompareMode((prev) => !prev);
|
||||
@@ -1526,6 +1530,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
||||
maps,
|
||||
diameterRange,
|
||||
elevationRange,
|
||||
forceStyleAutoApplyVersion,
|
||||
setForceStyleAutoApplyVersion,
|
||||
}}
|
||||
>
|
||||
<MapContext.Provider value={map}>
|
||||
|
||||
Reference in New Issue
Block a user