新增自动应用样式功能;强制计算后自动应用默认样式
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 isCompareMode = data?.isCompareMode ?? false;
|
||||||
const junctionText = data?.junctionText ?? "";
|
const junctionText = data?.junctionText ?? "";
|
||||||
const pipeText = data?.pipeText ?? "";
|
const pipeText = data?.pipeText ?? "";
|
||||||
|
const setForceStyleAutoApplyVersion = data?.setForceStyleAutoApplyVersion;
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
const [playInterval, setPlayInterval] = useState<number>(15000); // 毫秒
|
const [playInterval, setPlayInterval] = useState<number>(15000); // 毫秒
|
||||||
@@ -657,6 +658,7 @@ const Timeline: React.FC<TimelineProps> = ({
|
|||||||
});
|
});
|
||||||
// 清空当天当前时刻及之后的缓存并重新获取数据
|
// 清空当天当前时刻及之后的缓存并重新获取数据
|
||||||
clearCacheAndRefetch(calculationDate, calculationTime);
|
clearCacheAndRefetch(calculationDate, calculationTime);
|
||||||
|
setForceStyleAutoApplyVersion?.((prev) => prev + 1);
|
||||||
} else {
|
} else {
|
||||||
open?.({
|
open?.({
|
||||||
type: "error",
|
type: "error",
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import VectorLayer from "ol/layer/Vector";
|
|||||||
import { Style, Stroke, Fill, Circle } from "ol/style";
|
import { Style, Stroke, Fill, Circle } from "ol/style";
|
||||||
import Feature from "ol/Feature";
|
import Feature from "ol/Feature";
|
||||||
import StyleEditorPanel from "./StyleEditorPanel";
|
import StyleEditorPanel from "./StyleEditorPanel";
|
||||||
import { LayerStyleState } from "./StyleEditorPanel";
|
import { createDefaultLayerStyleStates } from "./styleEditorPresets";
|
||||||
|
import { LayerStyleState } from "./styleEditorTypes";
|
||||||
import StyleLegend from "./StyleLegend"; // 引入图例组件
|
import StyleLegend from "./StyleLegend"; // 引入图例组件
|
||||||
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
import { handleMapClickSelectFeatures as mapClickSelectFeatures } from "@/utils/mapQueryService";
|
||||||
import { useNotification } from "@refinedev/core";
|
import { useNotification } from "@refinedev/core";
|
||||||
@@ -90,81 +91,9 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式
|
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式
|
||||||
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>([
|
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>(
|
||||||
{
|
() => createDefaultLayerStyleStates()
|
||||||
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 activeLegendConfigs = layerStyleStates
|
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[];
|
maps?: OlMap[];
|
||||||
diameterRange?: [number, number];
|
diameterRange?: [number, number];
|
||||||
elevationRange?: [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 [showPipeId, setShowPipeId] = useState(false); // 控制管道ID显示
|
||||||
const [showContourLayer, setShowContourLayer] = useState(false); // 控制等高线图层显示
|
const [showContourLayer, setShowContourLayer] = useState(false); // 控制等高线图层显示
|
||||||
const [junctionText, setJunctionText] = useState("pressure");
|
const [junctionText, setJunctionText] = useState("pressure");
|
||||||
const [pipeText, setPipeText] = useState("flow");
|
const [pipeText, setPipeText] = useState("velocity");
|
||||||
const [contours, setContours] = useState<any[]>([]);
|
const [contours, setContours] = useState<any[]>([]);
|
||||||
const flowAnimation = useRef(false); // 添加动画控制标志
|
const flowAnimation = useRef(false); // 添加动画控制标志
|
||||||
const [isContourLayerAvailable, setContourLayerAvailable] = useState(false); // 控制等高线图层显示
|
const [isContourLayerAvailable, setContourLayerAvailable] = useState(false); // 控制等高线图层显示
|
||||||
@@ -263,6 +265,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
const [elevationRange, setElevationRange] = useState<
|
const [elevationRange, setElevationRange] = useState<
|
||||||
[number, number] | undefined
|
[number, number] | undefined
|
||||||
>();
|
>();
|
||||||
|
const [forceStyleAutoApplyVersion, setForceStyleAutoApplyVersion] =
|
||||||
|
useState(0);
|
||||||
|
|
||||||
const toggleCompareMode = useCallback(() => {
|
const toggleCompareMode = useCallback(() => {
|
||||||
setCompareMode((prev) => !prev);
|
setCompareMode((prev) => !prev);
|
||||||
@@ -1526,6 +1530,8 @@ const MapComponent: React.FC<MapComponentProps> = ({ children }) => {
|
|||||||
maps,
|
maps,
|
||||||
diameterRange,
|
diameterRange,
|
||||||
elevationRange,
|
elevationRange,
|
||||||
|
forceStyleAutoApplyVersion,
|
||||||
|
setForceStyleAutoApplyVersion,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MapContext.Provider value={map}>
|
<MapContext.Provider value={map}>
|
||||||
|
|||||||
Reference in New Issue
Block a user