WholeProcessPlatform/frontend/src/modules/MapStudio/composables/useDrawTools.ts
2026-07-28 18:28:07 +08:00

1098 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { watch, onUnmounted, type Ref, h } from 'vue';
import { storeToRefs } from 'pinia';
import { Modal, Input } from 'ant-design-vue';
import OlMap from 'ol/Map';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import Feature from 'ol/Feature';
import Point from 'ol/geom/Point';
import LineString from 'ol/geom/LineString';
import Polygon from 'ol/geom/Polygon';
import Circle from 'ol/geom/Circle';
import { fromLonLat, toLonLat } from 'ol/proj';
import { getLength, getArea } from 'ol/sphere';
import Style from 'ol/style/Style';
import Stroke from 'ol/style/Stroke';
import Fill from 'ol/style/Fill';
import CircleStyle from 'ol/style/Circle';
import Draw, { type DrawEvent } from 'ol/interaction/Draw';
import Select, { type SelectEvent } from 'ol/interaction/Select';
import Modify from 'ol/interaction/Modify';
import Translate from 'ol/interaction/Translate';
import Text from 'ol/style/Text';
import RegularShape from 'ol/style/RegularShape';
import { useMapStudioStore } from '../stores/mapStudioStore';
import { useUndoRedo } from './useUndoRedo';
import type { DrawStyle, SelectedDrawInfo, DrawToolType } from '../types';
/** 已绘制 feature 的自定义属性 key */
export const DRAW_FEATURE_ID_KEY = '_drawId';
export const DRAW_FEATURE_TYPE_KEY = '_drawType';
export const DRAW_FEATURE_TEXT_KEY = '_drawText';
let drawIdCounter = 0;
function nextDrawId(): string {
return `draw_${Date.now()}_${++drawIdCounter}`;
}
/** Feature 上存储单个样式对象的属性 key */
const DS_FEATURE_KEY = '_drawPerStyle';
/**
* 防止选中图形时同步样式触发不必要撤回步骤的标记。
* updateSelectedDrawInfo 中设为 true等待 Vue watch 队列消费完毕后自动恢复 false。
*/
let skipStyleCp = false;
/**
* 样式变更的防抖定时器150ms
* 同一交互内的快速连续变更(如拖拽颜色滑块)合并为 1 个撤回步骤;
* 不同用户操作(如修改颜色后修改字号)之间创建独立的撤回步骤。
*/
let styleCpTimer: ReturnType<typeof setTimeout> | null = null;
/** 根据 lineType 返回 lineDash 数组 */
function getLineDash(type: string): number[] | undefined {
switch (type) {
case 'dashed':
return [10, 6];
case 'dotted':
return [3, 4];
case 'dash-dot':
return [10, 4, 3, 4];
default:
return undefined;
}
}
/**
* 管理自定义绘制工具Draw/Modify/Select/Translate
*
* 设计:
* - Select / Modify / Translate 始终存在(永久交互),非绘制状态也能选中/编辑
* - Draw 交互跟随 drawToolActive 增删,光标也由其驱动
* - 每个绘制的 feature 独立存储自己的样式对象,右侧抽屉修改仅影响当前选中的 feature
*/
/** 模块级 action 引用,由 useDrawTools 填充 */
export const drawActions = {
deleteSelectedDraw: null as (() => void) | null,
updateSelectedTextContent: null as ((text: string) => void) | null,
getSerializedFeatures: null as (() => any[]) | null,
clearAllDrawings: null as (() => void) | null,
restoreDrawingsFromSave: null as
| ((features: any[], style: any) => void)
| null
};
export function useDrawTools(mapInstance: Ref<OlMap | null>) {
const store = useMapStudioStore();
const {
drawToolActive,
drawStyle,
selectedDrawInfo,
activeTab,
selectedDrawStyle,
selectedDrawText
} = storeToRefs(store);
// ── 图层 ──
const drawSource = new VectorSource();
const drawLayer = new VectorLayer({
source: drawSource,
style: createDrawStyleFunction,
zIndex: 100
});
// 绘制图形增删即时记录撤回步骤
// ⚠ OL 在 drawend 事件触发时尚未将图形添加到 source
// 因此不能依赖 drawend 内调用 checkpoint会捕获添加前的状态导致栈落后1个图形
// 改用 addfeature 事件OL 将 finalized feature 添加到 source 后触发)来记录添加步骤
const {
drawCheckpoint: onDrawChange,
setSuppressWatcher,
beginDrawBatch
} = useUndoRedo();
drawSource.on('addfeature', evt => {
const addedFeat = (evt as any).feature;
// drawstart 阶段 OL 添加的 sketch feature 没有 DRAW_FEATURE_ID_KEY过滤掉
// drawend 完成后 OL 添加的 finalized feature 已有 DRAW_FEATURE_ID_KEY记录 checkpoint
// 文字工具跳过addfeature 触发时文字内容还未录入,等 onOk 确认后再手动创建快照
if (
addedFeat &&
addedFeat.get(DRAW_FEATURE_ID_KEY) &&
addedFeat.get(DRAW_FEATURE_TYPE_KEY) !== 'text'
) {
onDrawChange();
}
});
drawSource.on('removefeature', evt => {
const removedFeat = (evt as any).feature;
// 只有真实绘制的 feature 才触发 checkpoint过滤 OL 内部 sketch feature问题7修复
if (removedFeat && removedFeat.get(DRAW_FEATURE_ID_KEY)) {
onDrawChange();
}
});
// ── 永久交互Select / Modify / Translate
const persistentInteractions: import('ol/interaction/Interaction')[] = [];
let currentDrawInteraction: Draw | null = null;
// ── Shift 键状态(用于约束直线/正方形/正圆绘制)──
let shiftHeld = false;
/** 当前绘制的 sketch feature用于 shift 按住时强制更新预览) */
let sketchFeature: Feature | null = null;
/** 当前 geometryFunction用于 shift 按住时重新计算几何) */
let currentGeometryFunction:
| ((coords: number[][], geom?: any) => any)
| null = null;
/** 最后一次传递给 geometryFunction 的原始坐标(用于 shift 按住时重新计算) */
let lastRawSketchCoords: number[][] | null = null;
// ── 样式工厂(从 feature 自身读取样式fallback 到 drawStyle──
function createDrawStyleFunction(feature: Feature): Style[] {
const perStyle = feature.get(DS_FEATURE_KEY) as
| Partial<DrawStyle>
| undefined;
const strokeColor = perStyle?.strokeColor ?? drawStyle.value.strokeColor;
const strokeWidth = perStyle?.strokeWidth ?? drawStyle.value.strokeWidth;
const fillColor = perStyle?.fillColor ?? drawStyle.value.fillColor;
const pointColor = perStyle?.pointColor ?? drawStyle.value.pointColor;
const pointRadius = perStyle?.pointRadius ?? drawStyle.value.pointRadius;
const lineType = perStyle?.lineType ?? drawStyle.value.lineType;
const showArrow = perStyle?.showArrow ?? drawStyle.value.showArrow;
const arrowPosition =
perStyle?.arrowPosition ?? drawStyle.value.arrowPosition;
const arrowSize = perStyle?.arrowSize ?? drawStyle.value.arrowSize;
const fontSize = perStyle?.fontSize ?? drawStyle.value.fontSize;
const fontFamily = perStyle?.fontFamily ?? drawStyle.value.fontFamily;
const textColor = perStyle?.textColor ?? drawStyle.value.textColor;
const textStrokeColor =
perStyle?.textStrokeColor ?? drawStyle.value.textStrokeColor;
const textStrokeWidth =
perStyle?.textStrokeWidth ?? drawStyle.value.textStrokeWidth;
const fontBold = perStyle?.fontBold ?? drawStyle.value.fontBold;
const stroke =
strokeWidth > 0
? new Stroke({
color: strokeColor,
width: strokeWidth,
lineDash: getLineDash(lineType) || []
})
: undefined;
const geomType = feature.getGeometry()?.getType();
const textContent = feature.get(DRAW_FEATURE_TEXT_KEY) as
| string
| undefined;
const drawTypeKey = feature.get(DRAW_FEATURE_TYPE_KEY) as
| string
| undefined;
// ── 文字标注 ──
if (textContent) {
return [
new Style({
text: new Text({
text: textContent,
font: `${fontBold ? 'bold ' : ''}${Math.max(
fontSize,
12
)}px ${fontFamily}`,
fill: new Fill({ color: textColor }),
stroke:
textStrokeWidth > 0
? new Stroke({ color: textStrokeColor, width: textStrokeWidth })
: undefined,
offsetX: 0,
offsetY: 0
})
})
];
}
// ── Point标记点──
if (geomType === 'Point') {
const imgStyle = new CircleStyle({
radius: pointRadius,
fill: new Fill({ color: pointColor })
});
if (stroke) imgStyle.setStroke(stroke);
return [new Style({ image: imgStyle })];
}
const styles: Style[] = [];
// ── LineString折线──
if (geomType === 'LineString') {
if (stroke) styles.push(new Style({ stroke }));
// 箭头(自由手绘不显示箭头)
if (showArrow && drawTypeKey !== 'freehand') {
const lineGeom = feature.getGeometry() as LineString;
const coords = lineGeom.getCoordinates();
if (coords.length >= 2) {
const arrowR =
arrowSize === 'small' ? 6 : arrowSize === 'large' ? 12 : 9;
// 结束箭头
const last = coords[coords.length - 1];
const prev = coords[coords.length - 2];
const endAngle = Math.atan2(last[1] - prev[1], last[0] - prev[0]);
styles.push(
new Style({
geometry: new Point(last),
image: new RegularShape({
points: 3,
radius: arrowR,
rotation: endAngle - Math.PI / 2,
fill: new Fill({ color: strokeColor }),
stroke: new Stroke({ color: strokeColor, width: 1 })
})
})
);
// 起始箭头
if (arrowPosition === 'both' && coords.length >= 2) {
const first = coords[0];
const second = coords[1];
const startAngle = Math.atan2(
first[1] - second[1],
first[0] - second[0]
);
styles.push(
new Style({
geometry: new Point(first),
image: new RegularShape({
points: 3,
radius: arrowR,
rotation: startAngle - Math.PI / 2,
fill: new Fill({ color: strokeColor }),
stroke: new Stroke({ color: strokeColor, width: 1 })
})
})
);
}
}
}
}
// ── Polygon / Circle ──
if (geomType === 'Polygon' || geomType === 'Circle') {
const polyStyle = new Style({ fill: new Fill({ color: fillColor }) });
if (stroke) polyStyle.setStroke(stroke);
styles.push(polyStyle);
}
return styles.length > 0 ? styles : [];
}
// ── 计算几何信息 ──
function computeGeomInfo(feature: Feature): SelectedDrawInfo {
const geom = feature.getGeometry()!;
const geomType = geom.getType();
const id = (feature.get(DRAW_FEATURE_ID_KEY) as string) || nextDrawId();
const drawTypeKey = (feature.get(DRAW_FEATURE_TYPE_KEY) as string) || '';
const coords: number[] = [];
if (geomType === 'Point') {
const [lng, lat] = toLonLat((geom as Point).getCoordinates());
coords.push(lng, lat);
} else if (geomType === 'LineString') {
const line = geom as LineString;
line.getCoordinates().forEach(p => {
const [lng, lat] = toLonLat(p);
coords.push(lng, lat);
});
} else if (geomType === 'Polygon') {
const poly = geom as Polygon;
poly.getFirstCoordinate().forEach(p => {
const [lng, lat] = toLonLat(p);
coords.push(lng, lat);
});
} else if (geomType === 'Circle') {
const center = toLonLat((geom as Circle).getCenter());
coords.push(center[0], center[1]);
}
let length: number | undefined;
let area: number | undefined;
if (geomType === 'LineString') {
length = getLength(geom as LineString);
} else if (geomType === 'Polygon') {
area = getArea(geom as Polygon);
} else if (geomType === 'Circle') {
area = Math.PI * (geom as Circle).getRadius() ** 2;
}
let displayType: SelectedDrawInfo['type'] = 'Point';
if (geomType === 'LineString') displayType = 'LineString';
else if (geomType === 'Polygon') displayType = 'Polygon';
else if (geomType === 'Circle') displayType = 'Circle';
return { id, type: displayType, drawTypeKey, coords, length, area };
}
let selectedFeatureId: string | null = null;
/** 将 feature 存储的样式同步到 selectedDrawStyle */
function syncPerStyleToSelected(feature: Feature | null) {
if (!feature) {
selectedDrawStyle.value = { ...drawStyle.value };
selectedDrawText.value = '';
return;
}
const stored = feature.get(DS_FEATURE_KEY) as
| Partial<DrawStyle>
| undefined;
selectedDrawStyle.value = {
strokeColor: stored?.strokeColor ?? drawStyle.value.strokeColor,
strokeWidth: stored?.strokeWidth ?? drawStyle.value.strokeWidth,
fillColor: stored?.fillColor ?? drawStyle.value.fillColor,
pointColor: stored?.pointColor ?? drawStyle.value.pointColor,
pointRadius: stored?.pointRadius ?? drawStyle.value.pointRadius,
lineType: stored?.lineType ?? drawStyle.value.lineType,
showArrow: stored?.showArrow ?? drawStyle.value.showArrow,
arrowPosition: stored?.arrowPosition ?? drawStyle.value.arrowPosition,
arrowSize: stored?.arrowSize ?? drawStyle.value.arrowSize,
fontSize: stored?.fontSize ?? drawStyle.value.fontSize,
fontFamily: stored?.fontFamily ?? drawStyle.value.fontFamily,
textColor: stored?.textColor ?? drawStyle.value.textColor,
textStrokeColor:
stored?.textStrokeColor ?? drawStyle.value.textStrokeColor,
textStrokeWidth:
stored?.textStrokeWidth ?? drawStyle.value.textStrokeWidth,
fontBold: stored?.fontBold ?? drawStyle.value.fontBold
};
selectedDrawText.value =
(feature.get(DRAW_FEATURE_TEXT_KEY) as string) || '';
}
/** 强制刷新图层渲染 */
function refreshLayerStyle() {
drawLayer.changed();
}
/** 将 selectedDrawStyle 写回 feature 并刷新渲染 */
function saveSelectedDrawStyleToFeature() {
if (!selectedFeatureId) return;
const feature = getSelectedFeature();
if (!feature) return;
const s = selectedDrawStyle.value;
feature.set(DS_FEATURE_KEY, {
strokeColor: s.strokeColor,
strokeWidth: s.strokeWidth,
fillColor: s.fillColor,
pointColor: s.pointColor,
pointRadius: s.pointRadius,
lineType: s.lineType,
showArrow: s.showArrow,
arrowPosition: s.arrowPosition,
arrowSize: s.arrowSize,
fontSize: s.fontSize,
fontFamily: s.fontFamily,
textColor: s.textColor,
textStrokeColor: s.textStrokeColor,
textStrokeWidth: s.textStrokeWidth,
fontBold: s.fontBold
});
refreshLayerStyle();
// 用户主动编辑样式时创建撤回步骤(跳过选中 feature 时的首次同步)
// 防抖 200ms同一交互内的快速连续变更如拖拽颜色滑块合并为 1 个步骤,
// 停止操作 200ms 后自动创建撤回步骤;不同用户操作之间产生独立步骤。
if (!skipStyleCp) {
if (styleCpTimer) {
clearTimeout(styleCpTimer);
}
styleCpTimer = setTimeout(() => {
styleCpTimer = null;
onDrawChange();
}, 200);
}
}
// 监听 selectedDrawStyle 变化 → 自动保存到当前 feature
const stopPerStyleWatch = watch(
selectedDrawStyle,
() => {
saveSelectedDrawStyleToFeature();
},
{ deep: true }
);
// 监听 drawStyle全局默认变化 → 刷新渲染
const stopStyleWatch = watch(
drawStyle,
() => {
refreshLayerStyle();
},
{ deep: true }
);
// ── 更新 selectedDrawInfo ──
function updateSelectedDrawInfo(feature: Feature | null) {
// 选中/取消选中图形时同步样式,不应产生撤回步骤
skipStyleCp = true;
setTimeout(() => {
skipStyleCp = false;
}, 0);
if (!feature) {
selectedDrawInfo.value = null;
selectedFeatureId = null;
syncPerStyleToSelected(null);
return;
}
selectedFeatureId = feature.get(DRAW_FEATURE_ID_KEY) as string;
selectedDrawInfo.value = computeGeomInfo(feature);
syncPerStyleToSelected(feature);
}
// ── 删除选中的绘制图形 ──
function deleteSelectedDraw() {
if (!selectedFeatureId) return;
const feature = drawSource
.getFeatures()
.find(f => f.get(DRAW_FEATURE_ID_KEY) === selectedFeatureId);
if (feature) drawSource.removeFeature(feature);
selectedDrawInfo.value = null;
selectedFeatureId = null;
}
function getSelectedFeature(): Feature | null {
if (!selectedFeatureId) return null;
return (
drawSource
.getFeatures()
.find(f => f.get(DRAW_FEATURE_ID_KEY) === selectedFeatureId) || null
);
}
/** 更新选中 feature 的文字内容 */
function updateSelectedTextContent(text: string) {
if (!selectedFeatureId) return;
const feature = getSelectedFeature();
if (!feature) return;
feature.set(DRAW_FEATURE_TEXT_KEY, text);
refreshLayerStyle();
onDrawChange();
}
// ── 设置永久交互Select / Modify / Translate──────────────
function setupPersistentInteractions() {
if (persistentInteractions.length > 0) return; // 防重复
const map = mapInstance.value;
if (!map) return;
// ── Select ──
const selectInteraction = new Select({
layers: [drawLayer],
style: null
});
selectInteraction.on('select', (evt: SelectEvent) => {
const feat = evt.selected.length > 0 ? evt.selected[0] : null;
// 选中图形只切换 tab 展示内容不应产生撤回步骤问题6修复
beginDrawBatch();
updateSelectedDrawInfo(feat);
if (feat) {
activeTab.value = 'draw';
} else if (!evt.deselected.length) {
// 点击空白 → 取消选中
selectedDrawInfo.value = null;
selectedFeatureId = null;
}
});
map.addInteraction(selectInteraction);
persistentInteractions.push(selectInteraction);
// ── Modify ──
const modifyInteraction = new Modify({
source: drawSource,
style: null
});
map.addInteraction(modifyInteraction);
persistentInteractions.push(modifyInteraction);
// ── Translate ──
const translateInteraction = new Translate({
layers: [drawLayer]
});
translateInteraction.on('translating', () => {
if (selectedFeatureId) {
const feat = drawSource
.getFeatures()
.find(f => f.get(DRAW_FEATURE_ID_KEY) === selectedFeatureId);
if (feat) {
selectedDrawInfo.value = computeGeomInfo(feat);
}
}
});
map.addInteraction(translateInteraction);
persistentInteractions.push(translateInteraction);
}
// ── 同步 Draw 交互(跟随 drawToolActive─────────────────────
/** 根据当前工具生成 Draw 预览样式 */
function createDrawPreviewStyle(): Style[] {
const ds = drawStyle.value;
const isPointTool = drawToolActive.value === 'point';
const isLineTool =
drawToolActive.value === 'line' || drawToolActive.value === 'freehand';
const previewStroke =
ds.strokeWidth > 0
? new Stroke({
color: ds.strokeColor,
width: ds.strokeWidth,
lineDash: isLineTool ? getLineDash(ds.lineType) || [] : []
})
: undefined;
return [
new Style({
stroke: previewStroke,
fill: new Fill({ color: ds.fillColor }),
image: isPointTool
? new CircleStyle({
radius: ds.pointRadius,
fill: new Fill({ color: ds.pointColor }),
stroke: previewStroke || undefined
})
: undefined
})
];
}
// ── Shift 约束的 geometryFunction ──
/**
* 椭圆/正圆 geometryFunction
* 使用 type:'Circle'click-drag-release
* 第一点为外接矩形左上角,第二点为右下角
* 默认椭圆,按住 shift 为正圆
*/
function createEllipseGeometryFunction(): (
coords: number[][],
geom?: any
) => any {
return function (coords: number[][], geom?: any): any {
// 保存原始坐标,用于 Shift 按住时强制更新预览
lastRawSketchCoords = coords.map(c => [c[0], c[1]]);
if (coords.length < 2) {
const p = coords[0];
const pts = 64;
const ring: number[][] = [];
for (let i = 0; i <= pts; i++) {
const a = (i / pts) * 2 * Math.PI;
ring.push([p[0], p[1]]);
}
if (geom) {
geom.setCoordinates([ring]);
return geom;
}
return new Polygon([ring]);
}
// 左上角 (x1,y1) → 右下角 (x2,y2)
const x1 = coords[0][0],
y1 = coords[0][1];
const x2 = coords[1][0],
y2 = coords[1][1];
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
let rx = Math.abs(x2 - x1) / 2 || 1;
let ry = Math.abs(y2 - y1) / 2 || 1;
if (shiftHeld) {
const r = Math.max(rx, ry);
rx = r;
ry = r;
}
const pts = 64;
const ring: number[][] = [];
for (let i = 0; i <= pts; i++) {
const a = (i / pts) * 2 * Math.PI;
ring.push([cx + rx * Math.cos(a), cy + ry * Math.sin(a)]);
}
if (geom) {
geom.setCoordinates([ring]);
return geom;
}
return new Polygon([ring]);
};
}
/**
* 矩形/正方形 geometryFunction
* 第一点为左上角,第二点为右下角
* 默认矩形,按住 shift 为正方形
*/
function createRectGeometryFunction(): (
coords: number[][],
geom?: any
) => any {
return function (coords: number[][], geom?: any): any {
// 保存原始坐标,用于 Shift 按住时强制更新预览
lastRawSketchCoords = coords.map(c => [c[0], c[1]]);
if (coords.length < 2) {
const p = coords[0];
const ring: number[][] = [
[p[0], p[1]],
[p[0], p[1]],
[p[0], p[1]],
[p[0], p[1]],
[p[0], p[1]]
];
if (geom) {
geom.setCoordinates([ring]);
return geom;
}
return new Polygon([ring]);
}
// 左上角 (x1,y1) → 右下角 (x2,y2)
const x1 = coords[0][0],
y1 = coords[0][1];
let x2 = coords[1][0],
y2 = coords[1][1];
if (shiftHeld) {
const w = Math.abs(x2 - x1);
const h = Math.abs(y2 - y1);
const max = Math.max(w, h);
if (w > h) {
y2 = y1 + (y2 >= y1 ? 1 : -1) * max;
} else {
x2 = x1 + (x2 >= x1 ? 1 : -1) * max;
}
}
const ring: number[][] = [
[Math.min(x1, x2), Math.min(y1, y2)],
[Math.min(x1, x2), Math.max(y1, y2)],
[Math.max(x1, x2), Math.max(y1, y2)],
[Math.max(x1, x2), Math.min(y1, y2)],
[Math.min(x1, x2), Math.min(y1, y2)]
];
if (geom) {
geom.setCoordinates([ring]);
return geom;
}
return new Polygon([ring]);
};
}
function createShiftConstrainedGeometryFunction(): (
coords: number[][],
geom?: any
) => any {
return function (coords: number[][], geom?: any): any {
if (!geom) {
geom = new LineString(coords);
} else {
geom.setCoordinates(coords);
}
if (shiftHeld && coords.length >= 2) {
const c = geom.getCoordinates();
const last = c[c.length - 1];
const prev = c[c.length - 2];
const dx = Math.abs(last[0] - prev[0]);
const dy = Math.abs(last[1] - prev[1]);
if (dx > dy) {
last[1] = prev[1]; // 水平约束
} else {
last[0] = prev[0]; // 垂直约束
}
geom.setCoordinates(c);
}
return geom;
};
}
function syncDrawInteraction() {
const map = mapInstance.value;
if (!map) return;
// 暴力扫描移除所有 Draw 交互
const interactions = map.getInteractions().getArray();
for (let i = interactions.length - 1; i >= 0; i--) {
if (interactions[i] instanceof Draw) {
map.removeInteraction(interactions[i]);
}
}
currentDrawInteraction = null;
// 恢复默认光标
map.getViewport().style.cursor = '';
// 没有激活的工具 → 结束
if (!drawToolActive.value) return;
// 十字光标
map.getViewport().style.cursor = 'crosshair';
const tool = drawToolActive.value;
let drawType: string;
let geometryFunction: any | undefined;
currentGeometryFunction = null; // 重置,仅 rect/circle 会赋值
switch (tool) {
case 'point':
drawType = 'Point';
break;
case 'line':
drawType = 'LineString';
geometryFunction = createShiftConstrainedGeometryFunction();
break;
case 'polygon':
drawType = 'Polygon';
break;
case 'rect':
drawType = 'LineString';
geometryFunction = createRectGeometryFunction();
currentGeometryFunction = geometryFunction;
break;
case 'circle':
drawType = 'LineString';
geometryFunction = createEllipseGeometryFunction();
currentGeometryFunction = geometryFunction;
break;
case 'freehand':
drawType = 'LineString';
break;
case 'text':
drawType = 'Point';
break;
default:
return;
}
currentDrawInteraction = new Draw({
source: drawSource,
type: drawType as any,
geometryFunction,
maxPoints: tool === 'rect' || tool === 'circle' ? 2 : undefined,
freehand: tool === 'freehand',
freehandCondition: () => false,
condition: () => true,
style: createDrawPreviewStyle()
});
currentDrawInteraction.on('drawstart', (evt: DrawEvent) => {
sketchFeature = evt.feature;
lastRawSketchCoords = null;
});
currentDrawInteraction.on('drawend', () => {
sketchFeature = null;
lastRawSketchCoords = null;
currentGeometryFunction = null;
});
currentDrawInteraction.on('drawend', (evt: DrawEvent) => {
const feature = evt.feature;
feature.set(DRAW_FEATURE_ID_KEY, nextDrawId());
feature.set(DRAW_FEATURE_TYPE_KEY, tool);
// 如果是文字工具,弹出 ant-design-vue Modal 输入框
if (tool === 'text') {
feature.set(DS_FEATURE_KEY, {
strokeColor: drawStyle.value.strokeColor,
strokeWidth: drawStyle.value.strokeWidth,
fillColor: drawStyle.value.fillColor,
pointColor: drawStyle.value.pointColor,
pointRadius: drawStyle.value.pointRadius,
lineType: drawStyle.value.lineType,
showArrow: drawStyle.value.showArrow,
arrowPosition: drawStyle.value.arrowPosition,
arrowSize: drawStyle.value.arrowSize,
fontSize: drawStyle.value.fontSize,
fontFamily: drawStyle.value.fontFamily,
textColor: drawStyle.value.textColor,
textStrokeColor: drawStyle.value.textStrokeColor,
textStrokeWidth: drawStyle.value.textStrokeWidth,
fontBold: drawStyle.value.fontBold
});
// 先抑制 watchersuppressUntil避免 updateSelectedDrawInfo 触发的 checkpoint问题5修复
beginDrawBatch();
updateSelectedDrawInfo(feature);
activeTab.value = 'draw';
let tempText = '';
Modal.confirm({
title: '输入文字内容',
content: () =>
h(Input, {
placeholder: '请输入文字内容',
onInput: (e: any) => {
tempText = e.target?.value ?? '';
}
}),
onOk: () => {
if (tempText && tempText.trim()) {
feature.set(DRAW_FEATURE_TEXT_KEY, tempText.trim());
} else {
drawSource.removeFeature(feature);
selectedDrawInfo.value = null;
selectedFeatureId = null;
}
// 文字确认后手动创建撤回步骤addfeature 阶段已跳过),捕获带文字内容的状态
onDrawChange();
// 同步样式不应产生额外撤回步骤onDrawChange 已记录完整的文字+样式状态)
skipStyleCp = true;
setTimeout(() => {
skipStyleCp = false;
}, 0);
syncPerStyleToSelected(feature);
refreshLayerStyle();
},
onCancel: () => {
drawSource.removeFeature(feature);
selectedDrawInfo.value = null;
selectedFeatureId = null;
}
});
return;
}
// 存储当前默认样式到新绘制的 feature
feature.set(DS_FEATURE_KEY, {
strokeColor: drawStyle.value.strokeColor,
strokeWidth: drawStyle.value.strokeWidth,
fillColor: drawStyle.value.fillColor,
pointColor: drawStyle.value.pointColor,
pointRadius: drawStyle.value.pointRadius,
lineType: drawStyle.value.lineType,
showArrow: drawStyle.value.showArrow,
arrowPosition: drawStyle.value.arrowPosition,
arrowSize: drawStyle.value.arrowSize,
fontSize: drawStyle.value.fontSize,
fontFamily: drawStyle.value.fontFamily,
textColor: drawStyle.value.textColor,
textStrokeColor: drawStyle.value.textStrokeColor,
textStrokeWidth: drawStyle.value.textStrokeWidth,
fontBold: drawStyle.value.fontBold
});
beginDrawBatch();
updateSelectedDrawInfo(feature);
activeTab.value = 'draw';
// ⚠ 不再在此处调用 onDrawChange()
// OL 在 drawend 时尚未将图形添加到 sourcecheckpoint 会捕获添加前的状态。
// 改为由 addfeature 监听器(已在 setup 中注册)在 OL 添加图形后自动触发。
});
map.addInteraction(currentDrawInteraction);
}
// ── 监听工具切换 ──
const stopToolWatch = watch(drawToolActive, () => {
syncDrawInteraction();
// 绘制工具激活时禁用 Select/Modify/Translate防止与绘制交互冲突
const isDrawing = drawToolActive.value !== null;
for (const interaction of persistentInteractions) {
interaction.setActive(!isDrawing);
}
});
// ── 监听地图注入,添加图层 + 设置永久交互 ──
const stopLayerWatch = watch(
mapInstance,
map => {
if (!map) return;
const layers = map.getLayers();
if (!layers.getArray().includes(drawLayer)) {
layers.push(drawLayer);
}
setupPersistentInteractions();
syncDrawInteraction();
},
{ immediate: true }
);
// ── Shift 键监听 ──
/** 当 Shift 状态变化时,强制更新预览几何(让矩形/圆形立即受约束) */
function updateDrawPreviewGeometry() {
if (
!sketchFeature ||
!currentGeometryFunction ||
!lastRawSketchCoords ||
lastRawSketchCoords.length < 2
)
return;
const existingGeom = sketchFeature.getGeometry();
const newGeom = currentGeometryFunction(lastRawSketchCoords, existingGeom);
if (newGeom && newGeom !== existingGeom) {
sketchFeature.setGeometry(newGeom);
}
}
function onShiftDown(e: KeyboardEvent) {
if (e.key === 'Shift') {
shiftHeld = true;
updateDrawPreviewGeometry();
}
}
function onShiftUp(e: KeyboardEvent) {
if (e.key === 'Shift') {
shiftHeld = false;
updateDrawPreviewGeometry();
}
}
document.addEventListener('keydown', onShiftDown);
document.addEventListener('keyup', onShiftUp);
// ── 键盘 Delete 键删除选中图形 ──
function onKeyDown(e: KeyboardEvent) {
if (e.key !== 'Delete' && e.key !== 'Backspace') return;
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement
) {
return;
}
if (selectedFeatureId) deleteSelectedDraw();
}
document.addEventListener('keydown', onKeyDown);
// ── 清理 ──
function dispose() {
stopLayerWatch();
stopStyleWatch();
stopToolWatch();
stopPerStyleWatch();
if (styleCpTimer !== null) {
clearTimeout(styleCpTimer);
styleCpTimer = null;
}
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('keydown', onShiftDown);
document.removeEventListener('keyup', onShiftUp);
const map = mapInstance.value;
if (!map) return;
const interactions = map.getInteractions().getArray();
for (let i = interactions.length - 1; i >= 0; i--) {
if (interactions[i] instanceof Draw) {
map.removeInteraction(interactions[i]);
}
}
currentDrawInteraction = null;
for (const interaction of persistentInteractions) {
map.removeInteraction(interaction);
}
persistentInteractions.length = 0;
map.removeLayer(drawLayer);
}
onUnmounted(() => dispose());
// ── 从序列化数据创建几何体 ──
function createGeometry(
type: string,
coords: any,
radius?: number
): import('ol/geom/Geometry') | null {
try {
switch (type) {
case 'Point':
return new Point(fromLonLat(coords));
case 'LineString':
return new LineString(coords.map((p: number[]) => fromLonLat(p)));
case 'Polygon':
return new Polygon([coords.map((p: number[]) => fromLonLat(p))]);
case 'Circle': {
const center = fromLonLat(coords);
const circle = new Circle(center, radius || 1000);
return circle;
}
default:
return null;
}
} catch {
return null;
}
}
// ── 清空所有绘制图形(恢复时使用) ──
function clearAllDrawings() {
drawSource.clear();
}
// ── 从保存的数据恢复绘制图形(编辑时使用) ──
function restoreDrawingsFromSave(features: any[], globalStyle: any) {
// 确保绘制图层已添加到地图
if (mapInstance.value) {
const layers = mapInstance.value.getLayers();
if (!layers.getArray().includes(drawLayer)) {
layers.push(drawLayer);
}
}
for (const item of features) {
const geom = createGeometry(item.type, item.coordinates, item.radius);
if (!geom) continue;
const feature = new Feature({ geometry: geom });
feature.set(DRAW_FEATURE_ID_KEY, item.id || nextDrawId());
feature.set(DRAW_FEATURE_TYPE_KEY, item.drawTypeKey || '');
feature.set(DRAW_FEATURE_TEXT_KEY, item.text || '');
if (item.style && Object.keys(item.style).length > 0) {
feature.set(DS_FEATURE_KEY, { ...item.style });
}
drawSource.addFeature(feature);
}
}
// ── 序列化所有绘制图形(用于保存) ──
function getSerializedFeatures(): any[] {
return drawSource.getFeatures().map(f => {
const geom = f.getGeometry();
const geomType = geom?.getType();
let coords: any;
let radius = 0;
if (geomType === 'Point') {
coords = toLonLat((geom as Point).getCoordinates());
} else if (geomType === 'LineString') {
coords = (geom as LineString).getCoordinates().map(p => toLonLat(p));
} else if (geomType === 'Polygon') {
coords = (geom as Polygon).getCoordinates()[0].map(p => toLonLat(p));
} else if (geomType === 'Circle') {
const circle = geom as Circle;
coords = toLonLat(circle.getCenter());
radius = circle.getRadius();
}
const perStyle = f.get(DS_FEATURE_KEY) || {};
return {
id: f.get(DRAW_FEATURE_ID_KEY),
type: geomType,
drawTypeKey: f.get(DRAW_FEATURE_TYPE_KEY) || '',
text: f.get(DRAW_FEATURE_TEXT_KEY) || '',
coordinates: coords,
radius,
style: { ...perStyle }
};
});
}
// 填充模块级 action 引用
drawActions.deleteSelectedDraw = deleteSelectedDraw;
drawActions.updateSelectedTextContent = updateSelectedTextContent;
drawActions.getSerializedFeatures = getSerializedFeatures;
drawActions.clearAllDrawings = clearAllDrawings;
drawActions.restoreDrawingsFromSave = restoreDrawingsFromSave;
return {
drawLayer,
drawSource,
deleteSelectedDraw,
getSelectedFeature,
saveSelectedDrawStyleToFeature,
updateSelectedTextContent,
dispose
};
}