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
|
||
};
|
||
}
|