fix(map): stabilize tiled style rendering

This commit is contained in:
2026-07-17 15:12:10 +08:00
parent 59447a100c
commit 589cf45aa7
28 changed files with 3884 additions and 2461 deletions
@@ -0,0 +1,50 @@
import { parseApplyLayerStylePayload } from "./toolCallStyleHelpers";
describe("parseApplyLayerStylePayload", () => {
it("accepts a valid snake_case interval contract", () => {
expect(
parseApplyLayerStylePayload({
layer_id: "pipes",
style_config: {
property: "velocity",
classification_method: "custom_breaks",
segments: 3,
custom_breaks: [0, 1, 2, 3],
color_type: "custom",
custom_colors: ["#000000", "#777777", "#ffffff"],
},
}),
).toMatchObject({
layerId: "pipes",
resetToDefault: false,
styleConfig: { segments: 3, customBreaks: [0, 1, 2, 3] },
});
});
it("rejects invalid class counts and array cardinalities", () => {
expect(
parseApplyLayerStylePayload({
layer_id: "pipes",
style_config: { segments: 1, property: "velocity" },
}),
).toBeNull();
expect(
parseApplyLayerStylePayload({
layer_id: "junctions",
style_config: {
segments: 3,
custom_breaks: [0, 1, 2],
},
}),
).toBeNull();
});
it("keeps camelCase compatibility", () => {
expect(
parseApplyLayerStylePayload({
layerId: "junctions",
styleConfig: { opacity: 0.5, colorType: "gradient" },
}),
).toMatchObject({ layerId: "junctions", styleConfig: { opacity: 0.5 } });
});
});
+83 -6
View File
@@ -1,4 +1,9 @@
import type { StyleConfig, DefaultLayerStyleId } from "@components/olmap/core/Controls/styleEditorTypes";
import type {
ClassificationMethod,
ColorType,
StyleConfig,
DefaultLayerStyleId,
} from "@components/olmap/core/Controls/styleEditorTypes";
export type ApplyLayerStyleActionPayload = {
layerId: DefaultLayerStyleId;
@@ -48,6 +53,23 @@ const asStringArray = (value: unknown): string[] | undefined =>
.filter((item): item is string => item !== undefined)
: undefined;
const asClassificationMethod = (value: unknown): ClassificationMethod | undefined => {
const normalized = asString(value);
return normalized === "pretty_breaks" || normalized === "custom_breaks"
? normalized
: undefined;
};
const asColorType = (value: unknown): ColorType | undefined => {
const normalized = asString(value);
return normalized === "single" ||
normalized === "gradient" ||
normalized === "rainbow" ||
normalized === "custom"
? normalized
: undefined;
};
export const normalizeStyleLayerId = (value: unknown): DefaultLayerStyleId | null => {
const normalized = asString(value)?.toLowerCase();
if (normalized === "junctions" || normalized === "pipes") {
@@ -77,13 +99,25 @@ export const parseApplyLayerStylePayload = (
? (params.styleConfig as Record<string, unknown>)
: null;
const classificationValue =
rawStyleConfig?.classification_method ?? rawStyleConfig?.classificationMethod;
const colorTypeValue = rawStyleConfig?.color_type ?? rawStyleConfig?.colorType;
const segmentsValue = rawStyleConfig?.segments;
const segments = asNumber(segmentsValue);
if (
(classificationValue !== undefined && !asClassificationMethod(classificationValue)) ||
(colorTypeValue !== undefined && !asColorType(colorTypeValue)) ||
(segmentsValue !== undefined &&
(!Number.isInteger(segments) || (segments as number) < 2 || (segments as number) > 10))
) {
return null;
}
const styleConfig: Partial<StyleConfig> | undefined = rawStyleConfig
? {
property: asString(rawStyleConfig.property),
classificationMethod: asString(
rawStyleConfig.classification_method ?? rawStyleConfig.classificationMethod,
),
segments: asNumber(rawStyleConfig.segments),
classificationMethod: asClassificationMethod(classificationValue),
segments,
minSize: asNumber(rawStyleConfig.min_size ?? rawStyleConfig.minSize),
maxSize: asNumber(rawStyleConfig.max_size ?? rawStyleConfig.maxSize),
minStrokeWidth: asNumber(
@@ -95,7 +129,7 @@ export const parseApplyLayerStylePayload = (
fixedStrokeWidth: asNumber(
rawStyleConfig.fixed_stroke_width ?? rawStyleConfig.fixedStrokeWidth,
),
colorType: asString(rawStyleConfig.color_type ?? rawStyleConfig.colorType),
colorType: asColorType(colorTypeValue),
singlePaletteIndex: asNumber(
rawStyleConfig.single_palette_index ?? rawStyleConfig.singlePaletteIndex,
),
@@ -121,6 +155,49 @@ export const parseApplyLayerStylePayload = (
}
: undefined;
if (styleConfig) {
const numericValues = [
styleConfig.minSize,
styleConfig.maxSize,
styleConfig.minStrokeWidth,
styleConfig.maxStrokeWidth,
styleConfig.fixedStrokeWidth,
].filter((value): value is number => value !== undefined);
if (numericValues.some((value) => value <= 0)) return null;
const paletteIndexes: Array<[number | undefined, number]> = [
[styleConfig.singlePaletteIndex, 7],
[styleConfig.gradientPaletteIndex, 3],
[styleConfig.rainbowPaletteIndex, 2],
];
if (
paletteIndexes.some(
([index, length]) =>
index !== undefined &&
(!Number.isInteger(index) || index < 0 || index >= length),
)
) return null;
if (
styleConfig.opacity !== undefined &&
(styleConfig.opacity < 0 || styleConfig.opacity > 1)
) return null;
if (
styleConfig.customBreaks &&
styleConfig.customBreaks.some(
(value, index, values) => index > 0 && value <= values[index - 1],
)
) return null;
if (
styleConfig.segments !== undefined &&
styleConfig.customBreaks &&
styleConfig.customBreaks.length !== styleConfig.segments + 1
) return null;
if (
styleConfig.segments !== undefined &&
styleConfig.customColors &&
styleConfig.customColors.length !== styleConfig.segments
) return null;
}
const hasStyleOverrides =
styleConfig &&
Object.values(styleConfig).some((value) =>