diff --git a/src/components/chat/ChatToolCallBlock.tsx b/src/components/chat/ChatToolCallBlock.tsx index a7f2c79..a552a00 100644 --- a/src/components/chat/ChatToolCallBlock.tsx +++ b/src/components/chat/ChatToolCallBlock.tsx @@ -25,6 +25,11 @@ import { type ChatToolAction, } from "@/store/chatToolStore"; import type { ToolCall } from "./chatMessageSections"; +import { + APPLY_LAYER_STYLE_TOOL, + describeApplyLayerStyle, + parseApplyLayerStylePayload, +} from "./toolCallStyleHelpers"; /* ------------------------------------------------------------------ */ /* Interactive card rendered inside a chat bubble for tool actions */ @@ -137,6 +142,12 @@ const TOOL_META: Record = { actionLabel: "应用渲染", color: "#3b82f6", }, + [APPLY_LAYER_STYLE_TOOL]: { + label: "图层样式", + icon: , + actionLabel: "应用样式", + color: "#14b8a6", + }, }; /* ---------- helpers ---------- */ @@ -270,6 +281,10 @@ function getToolDescription(toolCall: ToolCall): string { case "render_junctions": { return (params.render_ref as string | undefined) ?? "渲染引用"; } + case APPLY_LAYER_STYLE_TOOL: { + const payload = parseApplyLayerStylePayload(params); + return payload ? describeApplyLayerStyle(payload) : "图层样式"; + } default: return ""; } @@ -403,6 +418,18 @@ function buildAction(toolCall: ToolCall): ChatToolAction | null { 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: return null; } diff --git a/src/components/chat/hooks/useAgentToolActions.ts b/src/components/chat/hooks/useAgentToolActions.ts index e8bd878..ff9191a 100644 --- a/src/components/chat/hooks/useAgentToolActions.ts +++ b/src/components/chat/hooks/useAgentToolActions.ts @@ -5,6 +5,11 @@ import { useCallback } from "react"; import { useChatToolStore, type ChatToolAction } from "@/store/chatToolStore"; import type { StreamEvent } from "@/lib/chatStream"; import type { AgentArtifact, AgentArtifactKind } from "../GlobalChatbox.types"; +import { + APPLY_LAYER_STYLE_TOOL, + describeApplyLayerStyle, + parseApplyLayerStylePayload, +} from "../toolCallStyleHelpers"; 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 { action: null, kind: "tool", diff --git a/src/components/chat/toolCallStyleHelpers.ts b/src/components/chat/toolCallStyleHelpers.ts new file mode 100644 index 0000000..578c097 --- /dev/null +++ b/src/components/chat/toolCallStyleHelpers.ts @@ -0,0 +1,150 @@ +import type { StyleConfig, DefaultLayerStyleId } from "@components/olmap/core/Controls/styleEditorTypes"; + +export type ApplyLayerStyleActionPayload = { + layerId: DefaultLayerStyleId; + resetToDefault: boolean; + styleConfig?: Partial; +}; + +export const APPLY_LAYER_STYLE_TOOL = "apply_layer_style"; + +const LAYER_LABELS: Record = { + 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, +): 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) + : params.styleConfig && typeof params.styleConfig === "object" + ? (params.styleConfig as Record) + : null; + + const styleConfig: Partial | 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} · 应用样式`; +}; diff --git a/src/components/olmap/core/Controls/StyleEditorPanel.tsx b/src/components/olmap/core/Controls/StyleEditorPanel.tsx index 946cd3a..afc1686 100644 --- a/src/components/olmap/core/Controls/StyleEditorPanel.tsx +++ b/src/components/olmap/core/Controls/StyleEditorPanel.tsx @@ -2,34 +2,25 @@ import React from "react"; import StyleEditorForm from "./StyleEditorForm"; import { createDefaultLayerStyleState, createDefaultLayerStyleStates } from "./styleEditorPresets"; -import { useStyleEditor } from "./useStyleEditor"; import { LayerStyleState, StyleConfig, StyleEditorPanelProps } from "./styleEditorTypes"; const StyleEditorPanel: React.FC = ({ - layerStyleStates, - setLayerStyleStates, + isReady, + 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) { return
Loading...
; } @@ -41,15 +32,15 @@ const StyleEditorPanel: React.FC = ({ styleConfig={styleConfig} setStyleConfig={setStyleConfig} availableProperties={availableProperties} - onLayerChange={handleLayerChange} - onPropertyChange={handlePropertyChange} - onClassificationMethodChange={handleClassificationMethodChange} - onSegmentsChange={handleSegmentsChange} - onCustomBreakChange={handleCustomBreakChange} - onCustomBreakBlur={handleCustomBreakBlur} - onColorTypeChange={handleColorTypeChange} - onApply={handleApply} - onReset={handleReset} + onLayerChange={onLayerChange} + onPropertyChange={onPropertyChange} + onClassificationMethodChange={onClassificationMethodChange} + onSegmentsChange={onSegmentsChange} + onCustomBreakChange={onCustomBreakChange} + onCustomBreakBlur={onCustomBreakBlur} + onColorTypeChange={onColorTypeChange} + onApply={onApply} + onReset={onReset} /> ); }; diff --git a/src/components/olmap/core/Controls/Toolbar.tsx b/src/components/olmap/core/Controls/Toolbar.tsx index 333434e..772123d 100644 --- a/src/components/olmap/core/Controls/Toolbar.tsx +++ b/src/components/olmap/core/Controls/Toolbar.tsx @@ -24,6 +24,7 @@ import { buildFeatureProperties, } from "./toolbarFeatureHelpers"; import { useToolbarChatActions } from "./useToolbarChatActions"; +import { useStyleEditor } from "./useStyleEditor"; import { config } from "@/config/config"; import { apiFetch } from "@/lib/apiFetch"; @@ -81,20 +82,27 @@ const Toolbar: React.FC = ({ endTime?: string; } | null>(null); + // 样式状态管理 - 在 Toolbar 中管理,带有默认样式 + const [layerStyleStates, setLayerStyleStates] = useState( + () => createDefaultLayerStyleStates() + ); + const styleEditor = useStyleEditor({ + layerStyleStates, + setLayerStyleStates, + }); + useToolbarChatActions({ setHighlightFeatures, setChatPanelFeatureInfos, setChatPanelType, setChatPanelTimeRange, setShowHistoryPanel, + setShowStyleEditor, setActiveTools, + applyExternalStyle: styleEditor.applyExternalStyle, + resetExternalStyle: styleEditor.resetExternalStyle, }); - // 样式状态管理 - 在 Toolbar 中管理,带有默认样式 - const [layerStyleStates, setLayerStyleStates] = useState( - () => createDefaultLayerStyleStates() - ); - // 计算激活的图例配置 const activeLegendConfigs = layerStyleStates .filter((state) => state.isActive && state.legendConfig.property) @@ -444,8 +452,21 @@ const Toolbar: React.FC = ({ {showDrawPanel && map && }
>; } @@ -60,3 +60,7 @@ export interface StyleEditorFormProps { onApply: () => void; onReset: () => void; } + +export interface StyleEditorPanelProps extends StyleEditorFormProps { + isReady: boolean; +} diff --git a/src/components/olmap/core/Controls/useStyleEditor.ts b/src/components/olmap/core/Controls/useStyleEditor.ts index 1da8a3f..d317cd3 100644 --- a/src/components/olmap/core/Controls/useStyleEditor.ts +++ b/src/components/olmap/core/Controls/useStyleEditor.ts @@ -25,8 +25,10 @@ import { } from "./styleEditorUtils"; import { AvailableProperty, + DefaultLayerStyleId, LayerStyleState, - StyleEditorPanelProps, + StyleConfig, + StyleEditorStateProps, } from "./styleEditorTypes"; import { LegendStyleConfig } from "./StyleLegend"; import { calculateClassification } from "@utils/breaks_classification"; @@ -36,7 +38,7 @@ const UNIT_HEADLOSS_RANGE: [number, number] = [0, 5]; export const useStyleEditor = ({ layerStyleStates, setLayerStyleStates, -}: StyleEditorPanelProps) => { +}: StyleEditorStateProps) => { const map = useMap(); const data = useData(); const { open } = useNotification(); @@ -85,6 +87,51 @@ export const useStyleEditor = ({ latestLayerStyleStatesRef.current = 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( (layerId: string) => activeMaps.flatMap((targetMap) => @@ -191,28 +238,9 @@ export const useStyleEditor = ({ 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]; - }); + upsertLayerStyleState(newStyleState); }, - [availableProperties, selectedRenderLayer, setLayerStyleStates, styleConfig] + [availableProperties, selectedRenderLayer, styleConfig, upsertLayerStyleState] ); const applyContourLayerStyle = useCallback( @@ -520,9 +548,7 @@ export const useStyleEditor = ({ setShowJunctionTextLayer?.(styleConfig.showLabels); setShowJunctionId?.(styleConfig.showId); setApplyJunctionStyle(true); - if (property === "pressure") { - setContourLayerAvailable?.(true); - } + setContourLayerAvailable?.(property === "pressure"); saveLayerStyle(layerId); open?.({ type: "success", @@ -571,7 +597,7 @@ export const useStyleEditor = ({ targetLayer.setStyle(defaultFlatStyle); }); - setLayerStyleStates((prev) => prev.filter((state) => state.layerId !== layerId)); + removeLayerStyleState(layerId); if (layerId === "junctions") { setApplyJunctionStyle(false); @@ -593,7 +619,7 @@ export const useStyleEditor = ({ setContourLayerAvailable, setContours, setJunctionText, - setLayerStyleStates, + removeLayerStyleState, setPipeText, setShowJunctionId, setShowJunctionTextLayer, @@ -602,6 +628,211 @@ export const useStyleEditor = ({ setWaterflowLayerAvailable, ]); + const normalizeExternalStyleConfig = useCallback( + (layerId: DefaultLayerStyleId, overrides?: Partial): 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) => { + 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( (index: number) => { const newLayer = index >= 0 ? renderLayers[index] : undefined; @@ -747,6 +978,7 @@ export const useStyleEditor = ({ nextStates[index] = defaultState; } }); + latestLayerStyleStatesRef.current = nextStates; return nextStates; }); @@ -940,5 +1172,7 @@ export const useStyleEditor = ({ handleColorTypeChange, handleApply, handleReset, + applyExternalStyle, + resetExternalStyle, }; }; diff --git a/src/components/olmap/core/Controls/useToolbarChatActions.ts b/src/components/olmap/core/Controls/useToolbarChatActions.ts index bf9f3a9..f29a982 100644 --- a/src/components/olmap/core/Controls/useToolbarChatActions.ts +++ b/src/components/olmap/core/Controls/useToolbarChatActions.ts @@ -13,6 +13,7 @@ import { apiFetch } from "@/lib/apiFetch"; import { queryFeaturesByIds } from "@/utils/mapQueryService"; import { config } from "@/config/config"; import { useMap } from "../MapComponent"; +import type { DefaultLayerStyleId, StyleConfig } from "./styleEditorTypes"; type UseToolbarChatActionsParams = { setHighlightFeatures: Dispatch>; @@ -22,7 +23,13 @@ type UseToolbarChatActionsParams = { SetStateAction<{ startTime?: string; endTime?: string } | null> >; setShowHistoryPanel: Dispatch>; + setShowStyleEditor: Dispatch>; setActiveTools: Dispatch>; + applyExternalStyle: ( + layerId: DefaultLayerStyleId, + styleConfig?: Partial + ) => void; + resetExternalStyle: (layerId: DefaultLayerStyleId) => void; }; export const useToolbarChatActions = ({ @@ -31,7 +38,10 @@ export const useToolbarChatActions = ({ setChatPanelType, setChatPanelTimeRange, setShowHistoryPanel, + setShowStyleEditor, setActiveTools, + applyExternalStyle, + resetExternalStyle, }: UseToolbarChatActionsParams) => { const map = useMap(); const chatJunctionRenderCleanupRef = useRef<(() => void) | null>(null); @@ -206,17 +216,30 @@ export const useToolbarChatActions = ({ })(); 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, map, + resetExternalStyle, setActiveTools, setChatPanelFeatureInfos, setChatPanelTimeRange, setChatPanelType, setHighlightFeatures, setShowHistoryPanel, + setShowStyleEditor, ], ), ); diff --git a/src/store/chatToolStore.ts b/src/store/chatToolStore.ts index 317ffa4..7a0e4d4 100644 --- a/src/store/chatToolStore.ts +++ b/src/store/chatToolStore.ts @@ -1,5 +1,7 @@ import { create } from "zustand"; +import type { DefaultLayerStyleId, StyleConfig } from "@components/olmap/core/Controls/styleEditorTypes"; + /* ------------------------------------------------------------------ */ /* Chat Tool Action Store */ /* Decouples chat tool calls from map/panel execution. */ @@ -39,6 +41,12 @@ export type ChatToolAction = type: "render_junctions"; renderRef: string; sessionId?: string; + } + | { + type: "apply_layer_style"; + layerId: DefaultLayerStyleId; + resetToDefault: boolean; + styleConfig?: Partial; }; interface ChatToolState {