新增应用样式 agent 工具
This commit is contained in:
@@ -25,6 +25,11 @@ import {
|
|||||||
type ChatToolAction,
|
type ChatToolAction,
|
||||||
} from "@/store/chatToolStore";
|
} from "@/store/chatToolStore";
|
||||||
import type { ToolCall } from "./chatMessageSections";
|
import type { ToolCall } from "./chatMessageSections";
|
||||||
|
import {
|
||||||
|
APPLY_LAYER_STYLE_TOOL,
|
||||||
|
describeApplyLayerStyle,
|
||||||
|
parseApplyLayerStylePayload,
|
||||||
|
} from "./toolCallStyleHelpers";
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Interactive card rendered inside a chat bubble for tool actions */
|
/* Interactive card rendered inside a chat bubble for tool actions */
|
||||||
@@ -137,6 +142,12 @@ const TOOL_META: Record<string, ToolMeta> = {
|
|||||||
actionLabel: "应用渲染",
|
actionLabel: "应用渲染",
|
||||||
color: "#3b82f6",
|
color: "#3b82f6",
|
||||||
},
|
},
|
||||||
|
[APPLY_LAYER_STYLE_TOOL]: {
|
||||||
|
label: "图层样式",
|
||||||
|
icon: <LocationOnRounded sx={{ fontSize: 18 }} />,
|
||||||
|
actionLabel: "应用样式",
|
||||||
|
color: "#14b8a6",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---------- helpers ---------- */
|
/* ---------- helpers ---------- */
|
||||||
@@ -270,6 +281,10 @@ function getToolDescription(toolCall: ToolCall): string {
|
|||||||
case "render_junctions": {
|
case "render_junctions": {
|
||||||
return (params.render_ref as string | undefined) ?? "渲染引用";
|
return (params.render_ref as string | undefined) ?? "渲染引用";
|
||||||
}
|
}
|
||||||
|
case APPLY_LAYER_STYLE_TOOL: {
|
||||||
|
const payload = parseApplyLayerStylePayload(params);
|
||||||
|
return payload ? describeApplyLayerStyle(payload) : "图层样式";
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -403,6 +418,18 @@ function buildAction(toolCall: ToolCall): ChatToolAction | null {
|
|||||||
renderRef,
|
renderRef,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case APPLY_LAYER_STYLE_TOOL: {
|
||||||
|
const payload = parseApplyLayerStylePayload(params);
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "apply_layer_style",
|
||||||
|
layerId: payload.layerId,
|
||||||
|
resetToDefault: payload.resetToDefault,
|
||||||
|
styleConfig: payload.styleConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import { useCallback } from "react";
|
|||||||
import { useChatToolStore, type ChatToolAction } from "@/store/chatToolStore";
|
import { useChatToolStore, type ChatToolAction } from "@/store/chatToolStore";
|
||||||
import type { StreamEvent } from "@/lib/chatStream";
|
import type { StreamEvent } from "@/lib/chatStream";
|
||||||
import type { AgentArtifact, AgentArtifactKind } from "../GlobalChatbox.types";
|
import type { AgentArtifact, AgentArtifactKind } from "../GlobalChatbox.types";
|
||||||
|
import {
|
||||||
|
APPLY_LAYER_STYLE_TOOL,
|
||||||
|
describeApplyLayerStyle,
|
||||||
|
parseApplyLayerStylePayload,
|
||||||
|
} from "../toolCallStyleHelpers";
|
||||||
|
|
||||||
type ToolCallEvent = StreamEvent & { type: "tool_call" };
|
type ToolCallEvent = StreamEvent & { type: "tool_call" };
|
||||||
|
|
||||||
@@ -248,6 +253,23 @@ const buildToolAction = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tool === APPLY_LAYER_STYLE_TOOL) {
|
||||||
|
const payload = parseApplyLayerStylePayload(params);
|
||||||
|
return {
|
||||||
|
action: payload
|
||||||
|
? {
|
||||||
|
type: "apply_layer_style",
|
||||||
|
layerId: payload.layerId,
|
||||||
|
resetToDefault: payload.resetToDefault,
|
||||||
|
styleConfig: payload.styleConfig,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
kind: "map",
|
||||||
|
title: payload?.resetToDefault ? "重置图层样式" : "应用图层样式",
|
||||||
|
description: payload ? describeApplyLayerStyle(payload) : "图层样式",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: null,
|
action: null,
|
||||||
kind: "tool",
|
kind: "tool",
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import type { StyleConfig, DefaultLayerStyleId } from "@components/olmap/core/Controls/styleEditorTypes";
|
||||||
|
|
||||||
|
export type ApplyLayerStyleActionPayload = {
|
||||||
|
layerId: DefaultLayerStyleId;
|
||||||
|
resetToDefault: boolean;
|
||||||
|
styleConfig?: Partial<StyleConfig>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const APPLY_LAYER_STYLE_TOOL = "apply_layer_style";
|
||||||
|
|
||||||
|
const LAYER_LABELS: Record<DefaultLayerStyleId, string> = {
|
||||||
|
junctions: "节点",
|
||||||
|
pipes: "管道",
|
||||||
|
};
|
||||||
|
|
||||||
|
const asString = (value: unknown): string | undefined =>
|
||||||
|
typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||||
|
|
||||||
|
const asNumber = (value: unknown): number | undefined =>
|
||||||
|
typeof value === "number" && Number.isFinite(value)
|
||||||
|
? value
|
||||||
|
: typeof value === "string" && value.trim() && Number.isFinite(Number(value))
|
||||||
|
? Number(value)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const asBoolean = (value: unknown): boolean | undefined =>
|
||||||
|
typeof value === "boolean"
|
||||||
|
? value
|
||||||
|
: typeof value === "string"
|
||||||
|
? value === "true"
|
||||||
|
? true
|
||||||
|
: value === "false"
|
||||||
|
? false
|
||||||
|
: undefined
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const asNumberArray = (value: unknown): number[] | undefined =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
.map((item) => asNumber(item))
|
||||||
|
.filter((item): item is number => item !== undefined)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const asStringArray = (value: unknown): string[] | undefined =>
|
||||||
|
Array.isArray(value)
|
||||||
|
? value
|
||||||
|
.map((item) => asString(item))
|
||||||
|
.filter((item): item is string => item !== undefined)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
export const normalizeStyleLayerId = (value: unknown): DefaultLayerStyleId | null => {
|
||||||
|
const normalized = asString(value)?.toLowerCase();
|
||||||
|
if (normalized === "junctions" || normalized === "pipes") {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStyleLayerLabel = (layerId: DefaultLayerStyleId): string =>
|
||||||
|
LAYER_LABELS[layerId];
|
||||||
|
|
||||||
|
export const parseApplyLayerStylePayload = (
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
): ApplyLayerStyleActionPayload | null => {
|
||||||
|
const layerId = normalizeStyleLayerId(params.layer_id ?? params.layerId);
|
||||||
|
if (!layerId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetToDefault = Boolean(
|
||||||
|
asBoolean(params.reset_to_default ?? params.resetToDefault),
|
||||||
|
);
|
||||||
|
const rawStyleConfig =
|
||||||
|
params.style_config && typeof params.style_config === "object"
|
||||||
|
? (params.style_config as Record<string, unknown>)
|
||||||
|
: params.styleConfig && typeof params.styleConfig === "object"
|
||||||
|
? (params.styleConfig as Record<string, unknown>)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const styleConfig: Partial<StyleConfig> | undefined = rawStyleConfig
|
||||||
|
? {
|
||||||
|
property: asString(rawStyleConfig.property),
|
||||||
|
classificationMethod: asString(
|
||||||
|
rawStyleConfig.classification_method ?? rawStyleConfig.classificationMethod,
|
||||||
|
),
|
||||||
|
segments: asNumber(rawStyleConfig.segments),
|
||||||
|
minSize: asNumber(rawStyleConfig.min_size ?? rawStyleConfig.minSize),
|
||||||
|
maxSize: asNumber(rawStyleConfig.max_size ?? rawStyleConfig.maxSize),
|
||||||
|
minStrokeWidth: asNumber(
|
||||||
|
rawStyleConfig.min_stroke_width ?? rawStyleConfig.minStrokeWidth,
|
||||||
|
),
|
||||||
|
maxStrokeWidth: asNumber(
|
||||||
|
rawStyleConfig.max_stroke_width ?? rawStyleConfig.maxStrokeWidth,
|
||||||
|
),
|
||||||
|
fixedStrokeWidth: asNumber(
|
||||||
|
rawStyleConfig.fixed_stroke_width ?? rawStyleConfig.fixedStrokeWidth,
|
||||||
|
),
|
||||||
|
colorType: asString(rawStyleConfig.color_type ?? rawStyleConfig.colorType),
|
||||||
|
singlePaletteIndex: asNumber(
|
||||||
|
rawStyleConfig.single_palette_index ?? rawStyleConfig.singlePaletteIndex,
|
||||||
|
),
|
||||||
|
gradientPaletteIndex: asNumber(
|
||||||
|
rawStyleConfig.gradient_palette_index ?? rawStyleConfig.gradientPaletteIndex,
|
||||||
|
),
|
||||||
|
rainbowPaletteIndex: asNumber(
|
||||||
|
rawStyleConfig.rainbow_palette_index ?? rawStyleConfig.rainbowPaletteIndex,
|
||||||
|
),
|
||||||
|
showLabels: asBoolean(rawStyleConfig.show_labels ?? rawStyleConfig.showLabels),
|
||||||
|
showId: asBoolean(rawStyleConfig.show_id ?? rawStyleConfig.showId),
|
||||||
|
opacity: asNumber(rawStyleConfig.opacity),
|
||||||
|
adjustWidthByProperty: asBoolean(
|
||||||
|
rawStyleConfig.adjust_width_by_property ??
|
||||||
|
rawStyleConfig.adjustWidthByProperty,
|
||||||
|
),
|
||||||
|
customBreaks: asNumberArray(
|
||||||
|
rawStyleConfig.custom_breaks ?? rawStyleConfig.customBreaks,
|
||||||
|
),
|
||||||
|
customColors: asStringArray(
|
||||||
|
rawStyleConfig.custom_colors ?? rawStyleConfig.customColors,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const hasStyleOverrides =
|
||||||
|
styleConfig &&
|
||||||
|
Object.values(styleConfig).some((value) =>
|
||||||
|
Array.isArray(value) ? value.length > 0 : value !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!resetToDefault && !hasStyleOverrides) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
layerId,
|
||||||
|
resetToDefault,
|
||||||
|
styleConfig: hasStyleOverrides ? styleConfig : undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const describeApplyLayerStyle = (
|
||||||
|
payload: ApplyLayerStyleActionPayload,
|
||||||
|
): string => {
|
||||||
|
const layerLabel = getStyleLayerLabel(payload.layerId);
|
||||||
|
if (payload.resetToDefault) {
|
||||||
|
return `${layerLabel} · 重置默认样式`;
|
||||||
|
}
|
||||||
|
const property = payload.styleConfig?.property;
|
||||||
|
return property ? `${layerLabel} · ${property}` : `${layerLabel} · 应用样式`;
|
||||||
|
};
|
||||||
@@ -2,34 +2,25 @@ import React from "react";
|
|||||||
|
|
||||||
import StyleEditorForm from "./StyleEditorForm";
|
import StyleEditorForm from "./StyleEditorForm";
|
||||||
import { createDefaultLayerStyleState, createDefaultLayerStyleStates } from "./styleEditorPresets";
|
import { createDefaultLayerStyleState, createDefaultLayerStyleStates } from "./styleEditorPresets";
|
||||||
import { useStyleEditor } from "./useStyleEditor";
|
|
||||||
import { LayerStyleState, StyleConfig, StyleEditorPanelProps } from "./styleEditorTypes";
|
import { LayerStyleState, StyleConfig, StyleEditorPanelProps } from "./styleEditorTypes";
|
||||||
|
|
||||||
const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
||||||
layerStyleStates,
|
isReady,
|
||||||
setLayerStyleStates,
|
renderLayers,
|
||||||
|
selectedRenderLayer,
|
||||||
|
styleConfig,
|
||||||
|
setStyleConfig,
|
||||||
|
availableProperties,
|
||||||
|
onLayerChange,
|
||||||
|
onPropertyChange,
|
||||||
|
onClassificationMethodChange,
|
||||||
|
onSegmentsChange,
|
||||||
|
onCustomBreakChange,
|
||||||
|
onCustomBreakBlur,
|
||||||
|
onColorTypeChange,
|
||||||
|
onApply,
|
||||||
|
onReset,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
|
||||||
isReady,
|
|
||||||
renderLayers,
|
|
||||||
selectedRenderLayer,
|
|
||||||
styleConfig,
|
|
||||||
setStyleConfig,
|
|
||||||
availableProperties,
|
|
||||||
handleLayerChange,
|
|
||||||
handlePropertyChange,
|
|
||||||
handleClassificationMethodChange,
|
|
||||||
handleSegmentsChange,
|
|
||||||
handleCustomBreakChange,
|
|
||||||
handleCustomBreakBlur,
|
|
||||||
handleColorTypeChange,
|
|
||||||
handleApply,
|
|
||||||
handleReset,
|
|
||||||
} = useStyleEditor({
|
|
||||||
layerStyleStates,
|
|
||||||
setLayerStyleStates,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isReady) {
|
if (!isReady) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
@@ -41,15 +32,15 @@ const StyleEditorPanel: React.FC<StyleEditorPanelProps> = ({
|
|||||||
styleConfig={styleConfig}
|
styleConfig={styleConfig}
|
||||||
setStyleConfig={setStyleConfig}
|
setStyleConfig={setStyleConfig}
|
||||||
availableProperties={availableProperties}
|
availableProperties={availableProperties}
|
||||||
onLayerChange={handleLayerChange}
|
onLayerChange={onLayerChange}
|
||||||
onPropertyChange={handlePropertyChange}
|
onPropertyChange={onPropertyChange}
|
||||||
onClassificationMethodChange={handleClassificationMethodChange}
|
onClassificationMethodChange={onClassificationMethodChange}
|
||||||
onSegmentsChange={handleSegmentsChange}
|
onSegmentsChange={onSegmentsChange}
|
||||||
onCustomBreakChange={handleCustomBreakChange}
|
onCustomBreakChange={onCustomBreakChange}
|
||||||
onCustomBreakBlur={handleCustomBreakBlur}
|
onCustomBreakBlur={onCustomBreakBlur}
|
||||||
onColorTypeChange={handleColorTypeChange}
|
onColorTypeChange={onColorTypeChange}
|
||||||
onApply={handleApply}
|
onApply={onApply}
|
||||||
onReset={handleReset}
|
onReset={onReset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
buildFeatureProperties,
|
buildFeatureProperties,
|
||||||
} from "./toolbarFeatureHelpers";
|
} from "./toolbarFeatureHelpers";
|
||||||
import { useToolbarChatActions } from "./useToolbarChatActions";
|
import { useToolbarChatActions } from "./useToolbarChatActions";
|
||||||
|
import { useStyleEditor } from "./useStyleEditor";
|
||||||
|
|
||||||
import { config } from "@/config/config";
|
import { config } from "@/config/config";
|
||||||
import { apiFetch } from "@/lib/apiFetch";
|
import { apiFetch } from "@/lib/apiFetch";
|
||||||
@@ -81,20 +82,27 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
endTime?: string;
|
endTime?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
|
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式
|
||||||
|
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>(
|
||||||
|
() => createDefaultLayerStyleStates()
|
||||||
|
);
|
||||||
|
const styleEditor = useStyleEditor({
|
||||||
|
layerStyleStates,
|
||||||
|
setLayerStyleStates,
|
||||||
|
});
|
||||||
|
|
||||||
useToolbarChatActions({
|
useToolbarChatActions({
|
||||||
setHighlightFeatures,
|
setHighlightFeatures,
|
||||||
setChatPanelFeatureInfos,
|
setChatPanelFeatureInfos,
|
||||||
setChatPanelType,
|
setChatPanelType,
|
||||||
setChatPanelTimeRange,
|
setChatPanelTimeRange,
|
||||||
setShowHistoryPanel,
|
setShowHistoryPanel,
|
||||||
|
setShowStyleEditor,
|
||||||
setActiveTools,
|
setActiveTools,
|
||||||
|
applyExternalStyle: styleEditor.applyExternalStyle,
|
||||||
|
resetExternalStyle: styleEditor.resetExternalStyle,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 样式状态管理 - 在 Toolbar 中管理,带有默认样式
|
|
||||||
const [layerStyleStates, setLayerStyleStates] = useState<LayerStyleState[]>(
|
|
||||||
() => createDefaultLayerStyleStates()
|
|
||||||
);
|
|
||||||
|
|
||||||
// 计算激活的图例配置
|
// 计算激活的图例配置
|
||||||
const activeLegendConfigs = layerStyleStates
|
const activeLegendConfigs = layerStyleStates
|
||||||
.filter((state) => state.isActive && state.legendConfig.property)
|
.filter((state) => state.isActive && state.legendConfig.property)
|
||||||
@@ -444,8 +452,21 @@ const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
{showDrawPanel && map && <DrawPanel />}
|
{showDrawPanel && map && <DrawPanel />}
|
||||||
<div style={{ display: showStyleEditor ? "block" : "none" }}>
|
<div style={{ display: showStyleEditor ? "block" : "none" }}>
|
||||||
<StyleEditorPanel
|
<StyleEditorPanel
|
||||||
layerStyleStates={layerStyleStates}
|
isReady={styleEditor.isReady}
|
||||||
setLayerStyleStates={setLayerStyleStates}
|
renderLayers={styleEditor.renderLayers}
|
||||||
|
selectedRenderLayer={styleEditor.selectedRenderLayer}
|
||||||
|
styleConfig={styleEditor.styleConfig}
|
||||||
|
setStyleConfig={styleEditor.setStyleConfig}
|
||||||
|
availableProperties={styleEditor.availableProperties}
|
||||||
|
onLayerChange={styleEditor.handleLayerChange}
|
||||||
|
onPropertyChange={styleEditor.handlePropertyChange}
|
||||||
|
onClassificationMethodChange={styleEditor.handleClassificationMethodChange}
|
||||||
|
onSegmentsChange={styleEditor.handleSegmentsChange}
|
||||||
|
onCustomBreakChange={styleEditor.handleCustomBreakChange}
|
||||||
|
onCustomBreakBlur={styleEditor.handleCustomBreakBlur}
|
||||||
|
onColorTypeChange={styleEditor.handleColorTypeChange}
|
||||||
|
onApply={styleEditor.handleApply}
|
||||||
|
onReset={styleEditor.handleReset}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ToolbarHistoryPanel
|
<ToolbarHistoryPanel
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export interface LayerStyleState {
|
|||||||
|
|
||||||
export type DefaultLayerStyleId = "junctions" | "pipes";
|
export type DefaultLayerStyleId = "junctions" | "pipes";
|
||||||
|
|
||||||
export interface StyleEditorPanelProps {
|
export interface StyleEditorStateProps {
|
||||||
layerStyleStates: LayerStyleState[];
|
layerStyleStates: LayerStyleState[];
|
||||||
setLayerStyleStates: React.Dispatch<React.SetStateAction<LayerStyleState[]>>;
|
setLayerStyleStates: React.Dispatch<React.SetStateAction<LayerStyleState[]>>;
|
||||||
}
|
}
|
||||||
@@ -60,3 +60,7 @@ export interface StyleEditorFormProps {
|
|||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StyleEditorPanelProps extends StyleEditorFormProps {
|
||||||
|
isReady: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ import {
|
|||||||
} from "./styleEditorUtils";
|
} from "./styleEditorUtils";
|
||||||
import {
|
import {
|
||||||
AvailableProperty,
|
AvailableProperty,
|
||||||
|
DefaultLayerStyleId,
|
||||||
LayerStyleState,
|
LayerStyleState,
|
||||||
StyleEditorPanelProps,
|
StyleConfig,
|
||||||
|
StyleEditorStateProps,
|
||||||
} from "./styleEditorTypes";
|
} from "./styleEditorTypes";
|
||||||
import { LegendStyleConfig } from "./StyleLegend";
|
import { LegendStyleConfig } from "./StyleLegend";
|
||||||
import { calculateClassification } from "@utils/breaks_classification";
|
import { calculateClassification } from "@utils/breaks_classification";
|
||||||
@@ -36,7 +38,7 @@ const UNIT_HEADLOSS_RANGE: [number, number] = [0, 5];
|
|||||||
export const useStyleEditor = ({
|
export const useStyleEditor = ({
|
||||||
layerStyleStates,
|
layerStyleStates,
|
||||||
setLayerStyleStates,
|
setLayerStyleStates,
|
||||||
}: StyleEditorPanelProps) => {
|
}: StyleEditorStateProps) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const data = useData();
|
const data = useData();
|
||||||
const { open } = useNotification();
|
const { open } = useNotification();
|
||||||
@@ -85,6 +87,51 @@ export const useStyleEditor = ({
|
|||||||
latestLayerStyleStatesRef.current = layerStyleStates;
|
latestLayerStyleStatesRef.current = layerStyleStates;
|
||||||
}, [layerStyleStates]);
|
}, [layerStyleStates]);
|
||||||
|
|
||||||
|
const upsertLayerStyleState = useCallback(
|
||||||
|
(newStyleState: LayerStyleState) => {
|
||||||
|
const existingState = latestLayerStyleStatesRef.current.find(
|
||||||
|
(state) => state.layerId === newStyleState.layerId
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
existingState &&
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLayerStyleStates((prev) => {
|
||||||
|
const existingIndex = prev.findIndex(
|
||||||
|
(state) => state.layerId === newStyleState.layerId
|
||||||
|
);
|
||||||
|
const nextStates =
|
||||||
|
existingIndex === -1
|
||||||
|
? [...prev, newStyleState]
|
||||||
|
: prev.map((state, index) =>
|
||||||
|
index === existingIndex ? newStyleState : state
|
||||||
|
);
|
||||||
|
latestLayerStyleStatesRef.current = nextStates;
|
||||||
|
return nextStates;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setLayerStyleStates]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeLayerStyleState = useCallback(
|
||||||
|
(layerId: string) => {
|
||||||
|
setLayerStyleStates((prev) => {
|
||||||
|
const nextStates = prev.filter((state) => state.layerId !== layerId);
|
||||||
|
latestLayerStyleStatesRef.current = nextStates;
|
||||||
|
return nextStates;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setLayerStyleStates]
|
||||||
|
);
|
||||||
|
|
||||||
const getRenderLayersById = useCallback(
|
const getRenderLayersById = useCallback(
|
||||||
(layerId: string) =>
|
(layerId: string) =>
|
||||||
activeMaps.flatMap((targetMap) =>
|
activeMaps.flatMap((targetMap) =>
|
||||||
@@ -191,28 +238,9 @@ export const useStyleEditor = ({
|
|||||||
isActive: true,
|
isActive: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
setLayerStyleStates((prev) => {
|
upsertLayerStyleState(newStyleState);
|
||||||
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]
|
[availableProperties, selectedRenderLayer, styleConfig, upsertLayerStyleState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const applyContourLayerStyle = useCallback(
|
const applyContourLayerStyle = useCallback(
|
||||||
@@ -520,9 +548,7 @@ export const useStyleEditor = ({
|
|||||||
setShowJunctionTextLayer?.(styleConfig.showLabels);
|
setShowJunctionTextLayer?.(styleConfig.showLabels);
|
||||||
setShowJunctionId?.(styleConfig.showId);
|
setShowJunctionId?.(styleConfig.showId);
|
||||||
setApplyJunctionStyle(true);
|
setApplyJunctionStyle(true);
|
||||||
if (property === "pressure") {
|
setContourLayerAvailable?.(property === "pressure");
|
||||||
setContourLayerAvailable?.(true);
|
|
||||||
}
|
|
||||||
saveLayerStyle(layerId);
|
saveLayerStyle(layerId);
|
||||||
open?.({
|
open?.({
|
||||||
type: "success",
|
type: "success",
|
||||||
@@ -571,7 +597,7 @@ export const useStyleEditor = ({
|
|||||||
targetLayer.setStyle(defaultFlatStyle);
|
targetLayer.setStyle(defaultFlatStyle);
|
||||||
});
|
});
|
||||||
|
|
||||||
setLayerStyleStates((prev) => prev.filter((state) => state.layerId !== layerId));
|
removeLayerStyleState(layerId);
|
||||||
|
|
||||||
if (layerId === "junctions") {
|
if (layerId === "junctions") {
|
||||||
setApplyJunctionStyle(false);
|
setApplyJunctionStyle(false);
|
||||||
@@ -593,7 +619,7 @@ export const useStyleEditor = ({
|
|||||||
setContourLayerAvailable,
|
setContourLayerAvailable,
|
||||||
setContours,
|
setContours,
|
||||||
setJunctionText,
|
setJunctionText,
|
||||||
setLayerStyleStates,
|
removeLayerStyleState,
|
||||||
setPipeText,
|
setPipeText,
|
||||||
setShowJunctionId,
|
setShowJunctionId,
|
||||||
setShowJunctionTextLayer,
|
setShowJunctionTextLayer,
|
||||||
@@ -602,6 +628,211 @@ export const useStyleEditor = ({
|
|||||||
setWaterflowLayerAvailable,
|
setWaterflowLayerAvailable,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const normalizeExternalStyleConfig = useCallback(
|
||||||
|
(layerId: DefaultLayerStyleId, overrides?: Partial<StyleConfig>): StyleConfig => {
|
||||||
|
const currentStyleState = latestLayerStyleStatesRef.current.find(
|
||||||
|
(state) => state.layerId === layerId
|
||||||
|
);
|
||||||
|
const baseStyleConfig =
|
||||||
|
currentStyleState?.styleConfig || createDefaultLayerStyleState(layerId).styleConfig;
|
||||||
|
const nextStyleConfig: StyleConfig = {
|
||||||
|
...baseStyleConfig,
|
||||||
|
...overrides,
|
||||||
|
customBreaks: overrides?.customBreaks
|
||||||
|
? [...overrides.customBreaks]
|
||||||
|
: [...(baseStyleConfig.customBreaks || [])],
|
||||||
|
customColors: overrides?.customColors
|
||||||
|
? [...overrides.customColors]
|
||||||
|
: [...(baseStyleConfig.customColors || [])],
|
||||||
|
};
|
||||||
|
|
||||||
|
nextStyleConfig.segments = Math.max(1, Math.round(nextStyleConfig.segments || 1));
|
||||||
|
nextStyleConfig.opacity = Math.min(1, Math.max(0, nextStyleConfig.opacity));
|
||||||
|
nextStyleConfig.singlePaletteIndex = Math.max(
|
||||||
|
0,
|
||||||
|
Math.round(nextStyleConfig.singlePaletteIndex || 0)
|
||||||
|
);
|
||||||
|
nextStyleConfig.gradientPaletteIndex = Math.max(
|
||||||
|
0,
|
||||||
|
Math.round(nextStyleConfig.gradientPaletteIndex || 0)
|
||||||
|
);
|
||||||
|
nextStyleConfig.rainbowPaletteIndex = Math.max(
|
||||||
|
0,
|
||||||
|
Math.round(nextStyleConfig.rainbowPaletteIndex || 0)
|
||||||
|
);
|
||||||
|
nextStyleConfig.minSize = Math.max(1, nextStyleConfig.minSize);
|
||||||
|
nextStyleConfig.maxSize = Math.max(nextStyleConfig.minSize, nextStyleConfig.maxSize);
|
||||||
|
nextStyleConfig.minStrokeWidth = Math.max(1, nextStyleConfig.minStrokeWidth);
|
||||||
|
nextStyleConfig.maxStrokeWidth = Math.max(
|
||||||
|
nextStyleConfig.minStrokeWidth,
|
||||||
|
nextStyleConfig.maxStrokeWidth
|
||||||
|
);
|
||||||
|
nextStyleConfig.fixedStrokeWidth = Math.max(1, nextStyleConfig.fixedStrokeWidth);
|
||||||
|
nextStyleConfig.customColors =
|
||||||
|
nextStyleConfig.colorType === "custom"
|
||||||
|
? getDefaultCustomColors(
|
||||||
|
nextStyleConfig.segments,
|
||||||
|
nextStyleConfig.customColors || []
|
||||||
|
)
|
||||||
|
: nextStyleConfig.customColors;
|
||||||
|
nextStyleConfig.customBreaks =
|
||||||
|
nextStyleConfig.classificationMethod === "custom_breaks"
|
||||||
|
? normalizeCustomBreaks(
|
||||||
|
nextStyleConfig.customBreaks ||
|
||||||
|
getBreakDefaults(
|
||||||
|
nextStyleConfig.segments,
|
||||||
|
nextStyleConfig.property,
|
||||||
|
getRenderLayersById(layerId)[0]
|
||||||
|
),
|
||||||
|
nextStyleConfig.segments
|
||||||
|
)
|
||||||
|
: nextStyleConfig.customBreaks;
|
||||||
|
|
||||||
|
return nextStyleConfig;
|
||||||
|
},
|
||||||
|
[getBreakDefaults, getRenderLayersById]
|
||||||
|
);
|
||||||
|
|
||||||
|
const applyExternalStyle = useCallback(
|
||||||
|
(layerId: DefaultLayerStyleId, overrides?: Partial<StyleConfig>) => {
|
||||||
|
const targetLayer = getRenderLayersById(layerId)[0];
|
||||||
|
if (!targetLayer) {
|
||||||
|
open?.({
|
||||||
|
type: "error",
|
||||||
|
message: `未找到${layerId === "junctions" ? "节点" : "管道"}图层,无法应用样式。`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStyleConfig = normalizeExternalStyleConfig(layerId, overrides);
|
||||||
|
if (!nextStyleConfig.property) {
|
||||||
|
open?.({
|
||||||
|
type: "error",
|
||||||
|
message: "样式工具缺少有效的渲染属性,无法应用样式。",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerName = targetLayer.get("name") || (layerId === "junctions" ? "节点" : "管道");
|
||||||
|
const targetProperties = (targetLayer.get("properties") || []) as AvailableProperty[];
|
||||||
|
const propertyLabel =
|
||||||
|
targetProperties.find((item) => item.value === nextStyleConfig.property)?.name ||
|
||||||
|
nextStyleConfig.property;
|
||||||
|
|
||||||
|
setSelectedRenderLayer(targetLayer);
|
||||||
|
setStyleConfig(nextStyleConfig);
|
||||||
|
upsertLayerStyleState({
|
||||||
|
layerId,
|
||||||
|
layerName,
|
||||||
|
styleConfig: nextStyleConfig,
|
||||||
|
legendConfig: {
|
||||||
|
layerId,
|
||||||
|
layerName,
|
||||||
|
property: propertyLabel,
|
||||||
|
colors: [],
|
||||||
|
type: targetLayer.get("type") || (layerId === "junctions" ? "point" : "linestring"),
|
||||||
|
dimensions: [],
|
||||||
|
breaks: [],
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (layerId === "junctions") {
|
||||||
|
setJunctionText?.(nextStyleConfig.property);
|
||||||
|
setShowJunctionTextLayer?.(nextStyleConfig.showLabels);
|
||||||
|
setShowJunctionId?.(nextStyleConfig.showId);
|
||||||
|
setContourLayerAvailable?.(nextStyleConfig.property === "pressure");
|
||||||
|
setApplyJunctionStyle(true);
|
||||||
|
} else {
|
||||||
|
setPipeText?.(nextStyleConfig.property);
|
||||||
|
setShowPipeTextLayer?.(nextStyleConfig.showLabels);
|
||||||
|
setShowPipeId?.(nextStyleConfig.showId);
|
||||||
|
setWaterflowLayerAvailable?.(true);
|
||||||
|
setApplyPipeStyle(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
applyClassificationStyle(layerId, nextStyleConfig);
|
||||||
|
open?.({
|
||||||
|
type: "success",
|
||||||
|
message: `${layerId === "junctions" ? "节点" : "管道"}图层样式已应用。`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
applyClassificationStyle,
|
||||||
|
getRenderLayersById,
|
||||||
|
normalizeExternalStyleConfig,
|
||||||
|
open,
|
||||||
|
setContourLayerAvailable,
|
||||||
|
setJunctionText,
|
||||||
|
setPipeText,
|
||||||
|
setShowJunctionId,
|
||||||
|
setShowJunctionTextLayer,
|
||||||
|
setShowPipeId,
|
||||||
|
setShowPipeTextLayer,
|
||||||
|
setWaterflowLayerAvailable,
|
||||||
|
upsertLayerStyleState,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetExternalStyle = useCallback(
|
||||||
|
(layerId: DefaultLayerStyleId) => {
|
||||||
|
const targetLayer = getRenderLayersById(layerId)[0];
|
||||||
|
if (!targetLayer) {
|
||||||
|
open?.({
|
||||||
|
type: "error",
|
||||||
|
message: `未找到${layerId === "junctions" ? "节点" : "管道"}图层,无法重置样式。`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultStyleConfig = createDefaultLayerStyleState(layerId).styleConfig;
|
||||||
|
const defaultFlatStyle: FlatStyleLike = config.MAP_DEFAULT_STYLE;
|
||||||
|
|
||||||
|
setSelectedRenderLayer(targetLayer);
|
||||||
|
setStyleConfig(defaultStyleConfig);
|
||||||
|
|
||||||
|
getRenderLayersById(layerId).forEach((renderLayer) => {
|
||||||
|
renderLayer.setStyle(defaultFlatStyle);
|
||||||
|
});
|
||||||
|
|
||||||
|
removeLayerStyleState(layerId);
|
||||||
|
|
||||||
|
if (layerId === "junctions") {
|
||||||
|
setApplyJunctionStyle(false);
|
||||||
|
setShowJunctionTextLayer?.(false);
|
||||||
|
setShowJunctionId?.(false);
|
||||||
|
setJunctionText?.("");
|
||||||
|
setContours?.([]);
|
||||||
|
setContourLayerAvailable?.(false);
|
||||||
|
} else {
|
||||||
|
setApplyPipeStyle(false);
|
||||||
|
setShowPipeTextLayer?.(false);
|
||||||
|
setShowPipeId?.(false);
|
||||||
|
setPipeText?.("");
|
||||||
|
setWaterflowLayerAvailable?.(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
open?.({
|
||||||
|
type: "success",
|
||||||
|
message: `${layerId === "junctions" ? "节点" : "管道"}图层样式已重置。`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
getRenderLayersById,
|
||||||
|
open,
|
||||||
|
removeLayerStyleState,
|
||||||
|
setContourLayerAvailable,
|
||||||
|
setContours,
|
||||||
|
setJunctionText,
|
||||||
|
setPipeText,
|
||||||
|
setShowJunctionId,
|
||||||
|
setShowJunctionTextLayer,
|
||||||
|
setShowPipeId,
|
||||||
|
setShowPipeTextLayer,
|
||||||
|
setWaterflowLayerAvailable,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
const handleLayerChange = useCallback(
|
const handleLayerChange = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const newLayer = index >= 0 ? renderLayers[index] : undefined;
|
const newLayer = index >= 0 ? renderLayers[index] : undefined;
|
||||||
@@ -747,6 +978,7 @@ export const useStyleEditor = ({
|
|||||||
nextStates[index] = defaultState;
|
nextStates[index] = defaultState;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
latestLayerStyleStatesRef.current = nextStates;
|
||||||
return nextStates;
|
return nextStates;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -940,5 +1172,7 @@ export const useStyleEditor = ({
|
|||||||
handleColorTypeChange,
|
handleColorTypeChange,
|
||||||
handleApply,
|
handleApply,
|
||||||
handleReset,
|
handleReset,
|
||||||
|
applyExternalStyle,
|
||||||
|
resetExternalStyle,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { apiFetch } from "@/lib/apiFetch";
|
|||||||
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
import { queryFeaturesByIds } from "@/utils/mapQueryService";
|
||||||
import { config } from "@/config/config";
|
import { config } from "@/config/config";
|
||||||
import { useMap } from "../MapComponent";
|
import { useMap } from "../MapComponent";
|
||||||
|
import type { DefaultLayerStyleId, StyleConfig } from "./styleEditorTypes";
|
||||||
|
|
||||||
type UseToolbarChatActionsParams = {
|
type UseToolbarChatActionsParams = {
|
||||||
setHighlightFeatures: Dispatch<SetStateAction<Feature[]>>;
|
setHighlightFeatures: Dispatch<SetStateAction<Feature[]>>;
|
||||||
@@ -22,7 +23,13 @@ type UseToolbarChatActionsParams = {
|
|||||||
SetStateAction<{ startTime?: string; endTime?: string } | null>
|
SetStateAction<{ startTime?: string; endTime?: string } | null>
|
||||||
>;
|
>;
|
||||||
setShowHistoryPanel: Dispatch<SetStateAction<boolean>>;
|
setShowHistoryPanel: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowStyleEditor: Dispatch<SetStateAction<boolean>>;
|
||||||
setActiveTools: Dispatch<SetStateAction<string[]>>;
|
setActiveTools: Dispatch<SetStateAction<string[]>>;
|
||||||
|
applyExternalStyle: (
|
||||||
|
layerId: DefaultLayerStyleId,
|
||||||
|
styleConfig?: Partial<StyleConfig>
|
||||||
|
) => void;
|
||||||
|
resetExternalStyle: (layerId: DefaultLayerStyleId) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useToolbarChatActions = ({
|
export const useToolbarChatActions = ({
|
||||||
@@ -31,7 +38,10 @@ export const useToolbarChatActions = ({
|
|||||||
setChatPanelType,
|
setChatPanelType,
|
||||||
setChatPanelTimeRange,
|
setChatPanelTimeRange,
|
||||||
setShowHistoryPanel,
|
setShowHistoryPanel,
|
||||||
|
setShowStyleEditor,
|
||||||
setActiveTools,
|
setActiveTools,
|
||||||
|
applyExternalStyle,
|
||||||
|
resetExternalStyle,
|
||||||
}: UseToolbarChatActionsParams) => {
|
}: UseToolbarChatActionsParams) => {
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const chatJunctionRenderCleanupRef = useRef<(() => void) | null>(null);
|
const chatJunctionRenderCleanupRef = useRef<(() => void) | null>(null);
|
||||||
@@ -206,17 +216,30 @@ export const useToolbarChatActions = ({
|
|||||||
})();
|
})();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "apply_layer_style": {
|
||||||
|
setShowStyleEditor(true);
|
||||||
|
setActiveTools((prev) => (prev.includes("style") ? prev : [...prev, "style"]));
|
||||||
|
if (action.resetToDefault) {
|
||||||
|
resetExternalStyle(action.layerId);
|
||||||
|
} else {
|
||||||
|
applyExternalStyle(action.layerId, action.styleConfig);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
applyExternalStyle,
|
||||||
disposeChatJunctionRender,
|
disposeChatJunctionRender,
|
||||||
map,
|
map,
|
||||||
|
resetExternalStyle,
|
||||||
setActiveTools,
|
setActiveTools,
|
||||||
setChatPanelFeatureInfos,
|
setChatPanelFeatureInfos,
|
||||||
setChatPanelTimeRange,
|
setChatPanelTimeRange,
|
||||||
setChatPanelType,
|
setChatPanelType,
|
||||||
setHighlightFeatures,
|
setHighlightFeatures,
|
||||||
setShowHistoryPanel,
|
setShowHistoryPanel,
|
||||||
|
setShowStyleEditor,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
import type { DefaultLayerStyleId, StyleConfig } from "@components/olmap/core/Controls/styleEditorTypes";
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Chat Tool Action Store */
|
/* Chat Tool Action Store */
|
||||||
/* Decouples chat tool calls from map/panel execution. */
|
/* Decouples chat tool calls from map/panel execution. */
|
||||||
@@ -39,6 +41,12 @@ export type ChatToolAction =
|
|||||||
type: "render_junctions";
|
type: "render_junctions";
|
||||||
renderRef: string;
|
renderRef: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "apply_layer_style";
|
||||||
|
layerId: DefaultLayerStyleId;
|
||||||
|
resetToDefault: boolean;
|
||||||
|
styleConfig?: Partial<StyleConfig>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChatToolState {
|
interface ChatToolState {
|
||||||
|
|||||||
Reference in New Issue
Block a user