551 lines
17 KiB
TypeScript
551 lines
17 KiB
TypeScript
import {
|
|
Camera,
|
|
Circle,
|
|
Copy,
|
|
Database,
|
|
Dot,
|
|
Eraser,
|
|
Keyboard,
|
|
Layers3,
|
|
MapPinned,
|
|
Pentagon,
|
|
Redo2,
|
|
RefreshCw,
|
|
Route,
|
|
Ruler,
|
|
Share2,
|
|
Square,
|
|
Trash2,
|
|
Undo2,
|
|
Waypoints,
|
|
type LucideIcon
|
|
} from "lucide-react";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import {
|
|
MapAnnotationPanel,
|
|
MapActionRow,
|
|
MapControlPanel,
|
|
MapDrawToolbar,
|
|
MapLayerPanel,
|
|
MapLegendList,
|
|
MapMeasurePanel,
|
|
MapPanelSection,
|
|
type BaseLayerOption,
|
|
type MapAnnotationItem,
|
|
type MapAnnotationShareAction,
|
|
type MapLayerControlItem,
|
|
type MapLegendItem
|
|
} from "@/features/map/core";
|
|
import {
|
|
type WorkbenchDrawingAnnotation,
|
|
type WorkbenchDrawMode
|
|
} from "../hooks/use-workbench-drawing";
|
|
import {
|
|
type WorkbenchMeasurementResult,
|
|
type WorkbenchMeasureMode,
|
|
type WorkbenchMeasureUnit
|
|
} from "../hooks/use-workbench-measurement";
|
|
import { waterNetworkToolbarItems } from "../map/toolbar-config";
|
|
import type { WorkbenchGeocoding } from "../hooks/use-workbench-geocoding";
|
|
import { GeocodingPanel } from "./geocoding-panel";
|
|
import { FeatureLocationPanel, type FeatureLocationPanelProps } from "./feature-location-panel";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs";
|
|
|
|
export type ToolbarToolId = "geocoding" | "layers" | "measure" | "analysis" | "annotation" | "more";
|
|
export type ExportViewPreset = "current" | "4k";
|
|
|
|
type MeasureModeOption = {
|
|
id: WorkbenchMeasureMode;
|
|
label: string;
|
|
icon: LucideIcon;
|
|
};
|
|
|
|
const MEASURE_MODES: MeasureModeOption[] = [
|
|
{ id: "distance", label: "距离", icon: Route },
|
|
{ id: "area", label: "面积", icon: Pentagon },
|
|
{ id: "segment", label: "管段", icon: Waypoints }
|
|
];
|
|
|
|
const MEASURE_UNITS: Array<{ id: WorkbenchMeasureUnit; label: string }> = [
|
|
{ id: "m", label: "m" },
|
|
{ id: "km", label: "km" }
|
|
];
|
|
|
|
const ANNOTATION_SHARE_ACTIONS: MapAnnotationShareAction[] = [
|
|
{
|
|
id: "export",
|
|
icon: Share2,
|
|
label: "导出标注",
|
|
description: "GeoJSON / 截图报告"
|
|
}
|
|
];
|
|
|
|
const TOOL_PANEL_EXIT_MS = 160;
|
|
|
|
const DRAW_TOOL_OPTIONS: Array<{
|
|
id: WorkbenchDrawMode | "undo" | "redo" | "delete" | "clear";
|
|
label: string;
|
|
icon: LucideIcon;
|
|
tone?: "danger";
|
|
}> = [
|
|
{ id: "point", label: "点标注", icon: Dot },
|
|
{ id: "line", label: "线标注", icon: Route },
|
|
{ id: "polygon", label: "范围标注", icon: Pentagon },
|
|
{ id: "circle", label: "圆形标注", icon: Circle },
|
|
{ id: "undo", label: "撤销", icon: Undo2 },
|
|
{ id: "redo", label: "重做", icon: Redo2 },
|
|
{ id: "delete", label: "删除选中", icon: Trash2, tone: "danger" },
|
|
{ id: "clear", label: "清空全部", icon: Trash2, tone: "danger" }
|
|
];
|
|
|
|
export type ToolbarPanelProps = {
|
|
geocoding: WorkbenchGeocoding;
|
|
featureLocation: FeatureLocationPanelProps;
|
|
activeToolId: ToolbarToolId | null;
|
|
activeMeasureModeId: WorkbenchMeasureMode;
|
|
activeMeasureUnitId: WorkbenchMeasureUnit;
|
|
measuring: boolean;
|
|
measureResult: WorkbenchMeasurementResult;
|
|
measureEnabled: boolean;
|
|
hasMeasurePoints: boolean;
|
|
activeDrawToolId: WorkbenchDrawMode | null;
|
|
drawingEnabled: boolean;
|
|
drawingAnnotations: WorkbenchDrawingAnnotation[];
|
|
canUndoDrawing: boolean;
|
|
canRedoDrawing: boolean;
|
|
canDeleteSelectedDrawing: boolean;
|
|
hasDrawingFeatures: boolean;
|
|
layerItems: MapLayerControlItem[];
|
|
legendItems: MapLegendItem[];
|
|
baseLayerOptions: BaseLayerOption[];
|
|
activeBaseLayerId: string;
|
|
onSelectMeasureMode: (id: WorkbenchMeasureMode) => void;
|
|
onSelectMeasureUnit: (id: WorkbenchMeasureUnit) => void;
|
|
onStartMeasure: () => void;
|
|
onStopMeasure: () => void;
|
|
onClearMeasure: () => void;
|
|
onCopyMeasure: () => void;
|
|
onSelectDrawTool: (id: WorkbenchDrawMode | null) => void;
|
|
onUndoDrawing: () => void;
|
|
onRedoDrawing: () => void;
|
|
onDeleteSelectedDrawing: () => void;
|
|
onClearDrawing: () => void;
|
|
onExportAnnotations: () => void;
|
|
onSelectBaseLayer: (id: string) => void;
|
|
onToggleLayer: (id: string, visible: boolean) => void;
|
|
onExportView: (preset: ExportViewPreset) => void;
|
|
onRefreshTiles: () => void;
|
|
onShowDataStatus: () => void;
|
|
onShowShortcuts: () => void;
|
|
onClearMapOverlays: () => void;
|
|
panelWidthClassName?: string;
|
|
};
|
|
|
|
export function ToolbarPanel({
|
|
geocoding,
|
|
featureLocation,
|
|
activeToolId,
|
|
activeMeasureModeId,
|
|
activeMeasureUnitId,
|
|
measuring,
|
|
measureResult,
|
|
measureEnabled,
|
|
hasMeasurePoints,
|
|
activeDrawToolId,
|
|
drawingEnabled,
|
|
drawingAnnotations,
|
|
canUndoDrawing,
|
|
canRedoDrawing,
|
|
canDeleteSelectedDrawing,
|
|
hasDrawingFeatures,
|
|
layerItems,
|
|
legendItems,
|
|
baseLayerOptions,
|
|
activeBaseLayerId,
|
|
onSelectMeasureMode,
|
|
onSelectMeasureUnit,
|
|
onStartMeasure,
|
|
onStopMeasure,
|
|
onClearMeasure,
|
|
onCopyMeasure,
|
|
onSelectDrawTool,
|
|
onUndoDrawing,
|
|
onRedoDrawing,
|
|
onDeleteSelectedDrawing,
|
|
onClearDrawing,
|
|
onExportAnnotations,
|
|
onSelectBaseLayer,
|
|
onToggleLayer,
|
|
onExportView,
|
|
onRefreshTiles,
|
|
onShowDataStatus,
|
|
onShowShortcuts,
|
|
onClearMapOverlays,
|
|
panelWidthClassName
|
|
}: ToolbarPanelProps) {
|
|
const annotationItems = useMemo<MapAnnotationItem[]>(
|
|
() =>
|
|
drawingAnnotations.map((annotation) => ({
|
|
id: annotation.id,
|
|
icon: getDrawingAnnotationIcon(annotation.kind),
|
|
label: annotation.label,
|
|
description: annotation.description,
|
|
status: annotation.hidden ? "隐藏" : "显示",
|
|
selected: annotation.selected,
|
|
muted: annotation.hidden,
|
|
onClick: annotation.onToggleVisibility
|
|
})),
|
|
[drawingAnnotations]
|
|
);
|
|
const annotationShareActions = useMemo(
|
|
() =>
|
|
ANNOTATION_SHARE_ACTIONS.map((action) => ({
|
|
...action,
|
|
onClick: action.id === "export" ? onExportAnnotations : action.onClick
|
|
})),
|
|
[onExportAnnotations]
|
|
);
|
|
|
|
const [renderedToolId, setRenderedToolId] = useState<ToolbarToolId | null>(activeToolId);
|
|
const [locationMode, setLocationMode] = useState("address");
|
|
const panelVisible = Boolean(activeToolId);
|
|
|
|
useEffect(() => {
|
|
if (activeToolId) {
|
|
setRenderedToolId(activeToolId);
|
|
return;
|
|
}
|
|
|
|
if (!renderedToolId) {
|
|
return;
|
|
}
|
|
|
|
const exitTimer = window.setTimeout(() => {
|
|
setRenderedToolId(null);
|
|
}, TOOL_PANEL_EXIT_MS);
|
|
|
|
return () => window.clearTimeout(exitTimer);
|
|
}, [activeToolId, renderedToolId]);
|
|
|
|
if (!renderedToolId) {
|
|
return null;
|
|
}
|
|
|
|
const activeTool = waterNetworkToolbarItems.find((item) => item.id === renderedToolId);
|
|
const Icon = activeTool?.icon ?? Layers3;
|
|
|
|
if (renderedToolId === "geocoding") {
|
|
return (
|
|
<MapControlPanel
|
|
key="geocoding" title="定位" description={locationMode === "features" ? "按图层和要素 ID 定位" : geocoding.configured ? `${geocoding.region} · 天地图地名搜索` : "当前项目暂未配置地名搜索"} icon={Icon}
|
|
widthClassName={`${panelWidthClassName ?? "w-[324px] max-h-[calc(100dvh-13.25rem)]"} flex flex-col overflow-hidden`}
|
|
contentClassName="flex min-h-0 flex-1 flex-col"
|
|
visible={panelVisible}
|
|
>
|
|
<Tabs value={locationMode} onValueChange={(value) => {
|
|
if (value === "features" && geocoding.picking) geocoding.togglePicking();
|
|
setLocationMode(value);
|
|
}} className="flex min-h-0 flex-1 flex-col gap-3">
|
|
<TabsList aria-label="定位方式" className="surface-control grid shrink-0 grid-cols-2">
|
|
<TabsTrigger value="address">地址地名</TabsTrigger>
|
|
<TabsTrigger value="features">管网要素</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="address" forceMount hidden={locationMode !== "address"}
|
|
className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
<GeocodingPanel geocoding={geocoding} />
|
|
</TabsContent>
|
|
<TabsContent value="features" forceMount hidden={locationMode !== "features"}
|
|
className="min-h-0 flex-1 overflow-y-auto overscroll-contain pr-0.5">
|
|
<FeatureLocationPanel {...featureLocation} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</MapControlPanel>
|
|
);
|
|
}
|
|
|
|
if (renderedToolId === "layers") {
|
|
return (
|
|
<MapControlPanel
|
|
key="layers"
|
|
title="图层"
|
|
description={`${layerItems.filter((item) => item.visible).length}/${layerItems.length} 个业务图层可见`}
|
|
icon={Icon}
|
|
widthClassName={panelWidthClassName ?? "flex w-[324px] max-h-[calc(100dvh-13.25rem)] flex-col"}
|
|
contentClassName={panelWidthClassName ? undefined : "min-h-0 overflow-y-auto overscroll-contain pr-0.5"}
|
|
visible={panelVisible}
|
|
>
|
|
<MapLayerPanel
|
|
layerItems={layerItems}
|
|
baseLayerOptions={baseLayerOptions}
|
|
activeBaseLayerId={activeBaseLayerId}
|
|
onSelectBaseLayer={onSelectBaseLayer}
|
|
onToggleLayer={onToggleLayer}
|
|
/>
|
|
</MapControlPanel>
|
|
);
|
|
}
|
|
|
|
if (renderedToolId === "measure") {
|
|
const measureStatus = getMeasureStatus({
|
|
enabled: measureEnabled,
|
|
measuring,
|
|
hasPoints: hasMeasurePoints,
|
|
mode: activeMeasureModeId
|
|
});
|
|
|
|
return (
|
|
<MapControlPanel
|
|
key="measure"
|
|
title="测量"
|
|
description="距离、面积与管段测量"
|
|
icon={Icon}
|
|
widthClassName={panelWidthClassName ?? "flex w-[324px] max-h-[calc(100dvh-13.25rem)] flex-col"}
|
|
contentClassName={panelWidthClassName ? undefined : "min-h-0 overflow-y-auto overscroll-contain pr-0.5"}
|
|
visible={panelVisible}
|
|
>
|
|
<MapMeasurePanel
|
|
modes={MEASURE_MODES}
|
|
activeModeId={activeMeasureModeId}
|
|
resultLabel={measureResult.label}
|
|
resultValue={measureResult.value}
|
|
resultUnit={measureResult.unit}
|
|
metrics={measureResult.metrics}
|
|
units={MEASURE_UNITS}
|
|
activeUnitId={activeMeasureUnitId}
|
|
status={measureStatus}
|
|
actions={[
|
|
{
|
|
id: measuring ? "stop" : "start",
|
|
icon: measuring ? Square : Ruler,
|
|
label: measuring ? "停止测量" : "开始测量",
|
|
description:
|
|
measuring
|
|
? "暂停并保留当前读数"
|
|
: activeMeasureModeId === "segment"
|
|
? "在地图中选择管线"
|
|
: activeMeasureModeId === "area"
|
|
? "在地图中圈定范围"
|
|
: "在地图中连续取点",
|
|
variant: measuring ? "active" : "primary",
|
|
disabled: !measureEnabled,
|
|
onClick: measuring ? onStopMeasure : onStartMeasure
|
|
},
|
|
{
|
|
id: "copy",
|
|
icon: Copy,
|
|
label: "复制结果",
|
|
description: "复制当前读数",
|
|
disabled: !hasMeasurePoints,
|
|
onClick: onCopyMeasure
|
|
},
|
|
{
|
|
id: "clear",
|
|
icon: Trash2,
|
|
label: "清除测量",
|
|
description: activeMeasureModeId === "segment" ? "清空已选管段" : "移除临时测量线",
|
|
tone: "danger",
|
|
disabled: !hasMeasurePoints,
|
|
onClick: onClearMeasure
|
|
}
|
|
]}
|
|
onSelectMode={(id) => onSelectMeasureMode(id as WorkbenchMeasureMode)}
|
|
onSelectUnit={(id) => onSelectMeasureUnit(id as WorkbenchMeasureUnit)}
|
|
/>
|
|
</MapControlPanel>
|
|
);
|
|
}
|
|
|
|
if (renderedToolId === "analysis") {
|
|
return (
|
|
<MapControlPanel
|
|
key="analysis"
|
|
title="图例"
|
|
description={`${legendItems.length} 项当前地图符号`}
|
|
icon={Icon}
|
|
widthClassName={panelWidthClassName ?? "flex w-[324px] max-h-[calc(100dvh-13.25rem)] flex-col"}
|
|
contentClassName={panelWidthClassName ? undefined : "min-h-0 overflow-y-auto overscroll-contain pr-0.5"}
|
|
visible={panelVisible}
|
|
>
|
|
<LegendPanel legendItems={legendItems} />
|
|
</MapControlPanel>
|
|
);
|
|
}
|
|
|
|
if (renderedToolId === "annotation") {
|
|
return (
|
|
<MapControlPanel key="annotation" title="标注" description="创建与管理地图标注" icon={Icon} widthClassName={panelWidthClassName ?? "w-[312px]"} visible={panelVisible}>
|
|
<MapPanelSection title="绘制">
|
|
<MapDrawToolbar
|
|
variant="panel"
|
|
items={DRAW_TOOL_OPTIONS.map((item) => ({
|
|
...item,
|
|
active: activeDrawToolId === item.id,
|
|
disabled:
|
|
!drawingEnabled ||
|
|
(item.id === "undo" && !canUndoDrawing) ||
|
|
(item.id === "redo" && !canRedoDrawing) ||
|
|
(item.id === "delete" && !canDeleteSelectedDrawing) ||
|
|
(item.id === "clear" && !canUndoDrawing && !hasDrawingFeatures),
|
|
onClick: () => {
|
|
if (item.id === "undo") {
|
|
onUndoDrawing();
|
|
return;
|
|
}
|
|
|
|
if (item.id === "redo") {
|
|
onRedoDrawing();
|
|
return;
|
|
}
|
|
|
|
if (item.id === "delete") {
|
|
onDeleteSelectedDrawing();
|
|
return;
|
|
}
|
|
|
|
if (item.id === "clear") {
|
|
onClearDrawing();
|
|
return;
|
|
}
|
|
|
|
onSelectDrawTool(activeDrawToolId === item.id ? null : item.id);
|
|
}
|
|
}))}
|
|
/>
|
|
</MapPanelSection>
|
|
<MapAnnotationPanel annotations={annotationItems} shareActions={annotationShareActions} />
|
|
</MapControlPanel>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<MapControlPanel key="more" title="更多" description={activeTool?.description} icon={Icon} widthClassName={panelWidthClassName ?? "w-[292px]"} visible={panelVisible}>
|
|
<MorePanel
|
|
onExportView={onExportView}
|
|
onRefreshTiles={onRefreshTiles}
|
|
onShowDataStatus={onShowDataStatus}
|
|
onShowShortcuts={onShowShortcuts}
|
|
onClearMapOverlays={onClearMapOverlays}
|
|
/>
|
|
</MapControlPanel>
|
|
);
|
|
}
|
|
|
|
function getMeasureStatus({
|
|
enabled,
|
|
measuring,
|
|
hasPoints,
|
|
mode
|
|
}: {
|
|
enabled: boolean;
|
|
measuring: boolean;
|
|
hasPoints: boolean;
|
|
mode: WorkbenchMeasureMode;
|
|
}) {
|
|
if (!enabled) {
|
|
return {
|
|
label: "地图未就绪",
|
|
description: "等待地图加载后再开始测量",
|
|
tone: "disabled" as const
|
|
};
|
|
}
|
|
|
|
if (measuring) {
|
|
return {
|
|
label: "测量中",
|
|
description:
|
|
mode === "segment"
|
|
? "点击地图中的管线追加资产"
|
|
: mode === "area"
|
|
? "连续点击范围顶点,停止后保留读数"
|
|
: "连续点击地图取点,停止后保留读数",
|
|
tone: "active" as const
|
|
};
|
|
}
|
|
|
|
if (hasPoints) {
|
|
return {
|
|
label: "读数已保留",
|
|
description: "可继续测量、复制结果或清除",
|
|
tone: "ready" as const
|
|
};
|
|
}
|
|
|
|
return {
|
|
label: "等待开始",
|
|
description:
|
|
mode === "segment"
|
|
? "开始后点击管线选择资产"
|
|
: mode === "area"
|
|
? "开始后连续点击范围顶点"
|
|
: "开始后连续点击地图取点",
|
|
tone: "idle" as const
|
|
};
|
|
}
|
|
|
|
function LegendPanel({ legendItems }: { legendItems: MapLegendItem[] }) {
|
|
return (
|
|
<div className="space-y-2">
|
|
<MapLegendList items={legendItems} />
|
|
<div role="note" className="surface-control rounded-xl border px-3 py-2.5">
|
|
<p className="text-[11px] font-semibold text-slate-700">图例随图层显隐同步</p>
|
|
<p className="mt-1 text-[10px] leading-relaxed text-[var(--ink-muted)]">
|
|
管线颜色和线宽表示口径,设施颜色表示类型,风险色仅用于分析覆盖。
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type MorePanelProps = {
|
|
onClearMapOverlays: () => void;
|
|
onExportView: (preset: ExportViewPreset) => void;
|
|
onRefreshTiles: () => void;
|
|
onShowDataStatus: () => void;
|
|
onShowShortcuts: () => void;
|
|
};
|
|
|
|
function MorePanel({
|
|
onClearMapOverlays,
|
|
onExportView,
|
|
onRefreshTiles,
|
|
onShowDataStatus,
|
|
onShowShortcuts
|
|
}: MorePanelProps) {
|
|
return (
|
|
<div className="space-y-3">
|
|
<MapPanelSection title="清理地图">
|
|
<MapActionRow icon={Eraser} label="清空所有高亮和覆盖层" description="分析、测量、标注与定位标记" onClick={onClearMapOverlays} />
|
|
</MapPanelSection>
|
|
<MapPanelSection title="导出视图">
|
|
<MapActionRow icon={Camera} label="当前分辨率" description="按当前地图画布尺寸导出" onClick={() => onExportView("current")} />
|
|
<MapActionRow icon={Camera} label="4K 分辨率" description="长边约 3840px 的 PNG" onClick={() => onExportView("4k")} />
|
|
</MapPanelSection>
|
|
|
|
<MapPanelSection title="数据">
|
|
<MapActionRow icon={RefreshCw} label="刷新瓦片" description="请求地图重新渲染" onClick={onRefreshTiles} />
|
|
<MapActionRow icon={Database} label="数据状态" description="查看当前数据源状态" status="查看" onClick={onShowDataStatus} />
|
|
</MapPanelSection>
|
|
|
|
<MapPanelSection title="系统">
|
|
<MapActionRow icon={Keyboard} label="快捷键" description="查看地图操作键位" onClick={onShowShortcuts} />
|
|
</MapPanelSection>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function getDrawingAnnotationIcon(kind: WorkbenchDrawMode) {
|
|
if (kind === "line") {
|
|
return Route;
|
|
}
|
|
|
|
if (kind === "polygon") {
|
|
return Pentagon;
|
|
}
|
|
|
|
if (kind === "circle") {
|
|
return Circle;
|
|
}
|
|
|
|
return MapPinned;
|
|
}
|