1098 lines
36 KiB
TypeScript
1098 lines
36 KiB
TypeScript
|
|
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
|
|||
|
|
});
|
|||
|
|
// 先抑制 watcher(suppressUntil),避免 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 时尚未将图形添加到 source,checkpoint 会捕获添加前的状态。
|
|||
|
|
// 改为由 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
|
|||
|
|
};
|
|||
|
|
}
|