WholeProcessPlatform/frontend/src/modules/MapStudio/composables/useDrawTools.ts

1098 lines
36 KiB
TypeScript
Raw Normal View History

2026-07-28 18:28:07 +08:00
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
};
}