@loongbao-web-gis-utils/draw-utils-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DrawTool.d.ts +50 -0
- package/dist/DrawTool.js +358 -0
- package/dist/adapter/index.d.ts +48 -0
- package/dist/adapter/index.js +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/state-machine/index.d.ts +12 -0
- package/dist/state-machine/index.js +42 -0
- package/dist/types/config.d.ts +69 -0
- package/dist/types/config.js +7 -0
- package/dist/types/feature.d.ts +60 -0
- package/dist/types/feature.js +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +2 -0
- package/dist/types/state.d.ts +6 -0
- package/dist/types/state.js +7 -0
- package/dist/utils/index.d.ts +11 -0
- package/dist/utils/index.js +101 -0
- package/package.json +30 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FeatureInfo } from './types';
|
|
2
|
+
import { DrawState } from './types';
|
|
3
|
+
import type { DrawConfig } from './types';
|
|
4
|
+
import type { IGisAdapter, IBizCallbacks } from './adapter';
|
|
5
|
+
export interface DrawToolOptions {
|
|
6
|
+
adapter: IGisAdapter;
|
|
7
|
+
bizCallbacks?: IBizCallbacks;
|
|
8
|
+
}
|
|
9
|
+
export declare class DrawTool {
|
|
10
|
+
private adapter;
|
|
11
|
+
private renderer;
|
|
12
|
+
private bizCallbacks;
|
|
13
|
+
private stateMachine;
|
|
14
|
+
private config;
|
|
15
|
+
private featureMap;
|
|
16
|
+
private currentDrawing;
|
|
17
|
+
private drawnCount;
|
|
18
|
+
private roundFeatureIds;
|
|
19
|
+
private uiApp;
|
|
20
|
+
private uiMountEl;
|
|
21
|
+
private editingFeature;
|
|
22
|
+
private measureLabels;
|
|
23
|
+
constructor(options: DrawToolOptions);
|
|
24
|
+
onMouseMove(coord: [number, number]): void;
|
|
25
|
+
onClick(coord: [number, number]): void;
|
|
26
|
+
onDblClick(coord: [number, number]): Promise<void>;
|
|
27
|
+
startDraw(config: DrawConfig): void;
|
|
28
|
+
stopDraw(): void;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
getFeature(id: number): FeatureInfo | undefined;
|
|
31
|
+
getAllFeatures(): FeatureInfo[];
|
|
32
|
+
getState(): DrawState;
|
|
33
|
+
updateFeatureInfo(f: FeatureInfo): void;
|
|
34
|
+
private canEditFeature;
|
|
35
|
+
private canDeleteFeature;
|
|
36
|
+
private makeFeature;
|
|
37
|
+
private invokeInsert;
|
|
38
|
+
private editFeature;
|
|
39
|
+
private requestDeleteFeature;
|
|
40
|
+
private doDelete;
|
|
41
|
+
private updateMeasureLabels;
|
|
42
|
+
private clearMeasureLabels;
|
|
43
|
+
private log;
|
|
44
|
+
private showToast;
|
|
45
|
+
private showDeleteConfirm;
|
|
46
|
+
private showEditPanel;
|
|
47
|
+
private mountUi;
|
|
48
|
+
private dragPanel;
|
|
49
|
+
private destroyUi;
|
|
50
|
+
}
|
package/dist/DrawTool.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { DrawState } from './types';
|
|
2
|
+
import { DEFAULT_DRAW_CONFIG, DEFAULT_RECT_DETAILS, DEFAULT_TRIANGLE_DETAILS, DEFAULT_CIRCLE_DETAILS, DEFAULT_LINE_DETAILS, DEFAULT_POLYLINE_DETAILS, DEFAULT_SHAPE_DETAILS } from './types';
|
|
3
|
+
import { StateMachine } from './state-machine';
|
|
4
|
+
import { generateUniqueId, generateFeatureName, toWktPolygon, toWktLineString, geoDistance, geoArea, geoPolygonCenter, formatMeters, formatArea } from './utils';
|
|
5
|
+
import { createApp, h, ref, watch } from 'vue';
|
|
6
|
+
const PRESET_COLORS = ['#ef4444', '#f97316', '#eab308', '#22c55e', '#06b6d4', '#3b82f6', '#8b5cf6', '#ec4899', '#ffffff', '#000000', '#9ca3af', '#6b7280'];
|
|
7
|
+
function rgbaFromHex(h) { const x = h.replace('#', ''); return `rgba(${parseInt(x.substring(0, 2), 16)},${parseInt(x.substring(2, 4), 16)},${parseInt(x.substring(4, 6), 16)},1)`; }
|
|
8
|
+
function rgbaToHex(r) { const m = r.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (!m)
|
|
9
|
+
return '#000000'; return '#' + [m[1], m[2], m[3]].map(v => parseInt(v).toString(16).padStart(2, '0')).join(''); }
|
|
10
|
+
function buildCircleRing(cx, r, seg) { const ring = []; for (let i = 0; i < seg; i++) {
|
|
11
|
+
const a = 2 * Math.PI * i / seg;
|
|
12
|
+
ring.push([cx[0] + r * Math.cos(a), cx[1] + r * Math.sin(a)]);
|
|
13
|
+
} ; ring.push(ring[0]); return ring; }
|
|
14
|
+
function metersPerDeg(lat) { return 111320 * Math.cos(lat * Math.PI / 180); }
|
|
15
|
+
function parseWktLocal(wkt) { const m = wkt.match(/\((.*)\)/); if (!m)
|
|
16
|
+
return null; let inner = m[1].replace(/^\(/, '').replace(/\)$/, ''); return inner.split(',').map(p => { const [x, y] = p.trim().split(/\s+/).map(Number); return [x, y]; }); }
|
|
17
|
+
export class DrawTool {
|
|
18
|
+
adapter;
|
|
19
|
+
renderer;
|
|
20
|
+
bizCallbacks;
|
|
21
|
+
stateMachine = new StateMachine();
|
|
22
|
+
config = null;
|
|
23
|
+
featureMap = new Map();
|
|
24
|
+
currentDrawing = null;
|
|
25
|
+
drawnCount = 0;
|
|
26
|
+
roundFeatureIds = new Set();
|
|
27
|
+
uiApp = null;
|
|
28
|
+
uiMountEl = null;
|
|
29
|
+
editingFeature = null;
|
|
30
|
+
measureLabels = [];
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.adapter = options.adapter;
|
|
33
|
+
this.renderer = this.adapter.getFeatureRenderer();
|
|
34
|
+
this.bizCallbacks = options.bizCallbacks || {};
|
|
35
|
+
this.renderer.onHoverAction((action, f) => { if (action === 'edit')
|
|
36
|
+
this.editFeature(f.id);
|
|
37
|
+
else if (action === 'delete')
|
|
38
|
+
this.requestDeleteFeature(f.id); this.renderer.hideHoverActions(); });
|
|
39
|
+
}
|
|
40
|
+
onMouseMove(coord) {
|
|
41
|
+
if (this.stateMachine.isDrawing() && this.currentDrawing) {
|
|
42
|
+
const { type, points } = this.currentDrawing;
|
|
43
|
+
if (type === 'circle' && points.length >= 1)
|
|
44
|
+
this.renderer.updatePreview([points[0], coord], type);
|
|
45
|
+
else if (type === 'rect' && points.length >= 1)
|
|
46
|
+
this.renderer.updatePreview([points[0], coord], type);
|
|
47
|
+
else if (type === 'line' && points.length >= 1)
|
|
48
|
+
this.renderer.updatePreview([...points, coord], type);
|
|
49
|
+
else if ((type === 'polyline' || type === 'customShape') && points.length >= 1)
|
|
50
|
+
this.renderer.updatePreview([...points, coord], type);
|
|
51
|
+
else if (type === 'triangle' && points.length >= 1) {
|
|
52
|
+
if (points.length === 1)
|
|
53
|
+
this.renderer.updatePreview([points[0], coord, coord], type);
|
|
54
|
+
else
|
|
55
|
+
this.renderer.updatePreview([...points, coord], type);
|
|
56
|
+
}
|
|
57
|
+
this.updateMeasureLabels(type, points, coord);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (this.stateMachine.isEditing())
|
|
61
|
+
return;
|
|
62
|
+
if (this.stateMachine.isIdle()) {
|
|
63
|
+
const f = this.renderer.getFeatureAtCoordinate(coord);
|
|
64
|
+
this.renderer.highlightHoverFeature(f ? f.id : null);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
onClick(coord) {
|
|
68
|
+
if (this.stateMachine.isDrawing() && this.currentDrawing) {
|
|
69
|
+
const t = this.currentDrawing.type;
|
|
70
|
+
if (t === 'triangle' && this.currentDrawing.points.length === 1) {
|
|
71
|
+
this.currentDrawing.points.push(coord);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (t === 'polyline' || t === 'customShape') {
|
|
75
|
+
this.currentDrawing.points.push(coord);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (this.stateMachine.isEditing())
|
|
81
|
+
return;
|
|
82
|
+
if (this.stateMachine.isIdle()) {
|
|
83
|
+
const f = this.renderer.getFeatureAtCoordinate(coord);
|
|
84
|
+
if (f && (this.canEditFeature(f.id) || this.canDeleteFeature(f.id)))
|
|
85
|
+
this.renderer.showHoverActions(f, coord, { showEdit: this.canEditFeature(f.id), showDelete: this.canDeleteFeature(f.id) });
|
|
86
|
+
else
|
|
87
|
+
this.renderer.hideHoverActions();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async onDblClick(coord) {
|
|
92
|
+
if (this.stateMachine.isIdle()) {
|
|
93
|
+
if (this.renderer.getFeatureAtCoordinate(coord))
|
|
94
|
+
return;
|
|
95
|
+
const lim = this.config?.limit ?? 1;
|
|
96
|
+
if (!this.config || lim <= 0)
|
|
97
|
+
return;
|
|
98
|
+
if (this.drawnCount >= lim) {
|
|
99
|
+
if (this.config?.enableLimitMsg)
|
|
100
|
+
this.showToast('绘制元素数量超出限制');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.stateMachine.transitionTo(DrawState.DRAWING);
|
|
104
|
+
this.currentDrawing = { type: this.config.type, points: [coord] };
|
|
105
|
+
this.renderer.startDrawing(this.config.type);
|
|
106
|
+
this.log('DRAWING start', { type: this.config.type, point: coord, count: this.drawnCount + 1 });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!this.stateMachine.isDrawing() || !this.currentDrawing)
|
|
110
|
+
return;
|
|
111
|
+
const { type, points } = this.currentDrawing;
|
|
112
|
+
if (type === 'customShape' || type === 'triangle') {
|
|
113
|
+
const all = [...points, coord];
|
|
114
|
+
const u = new Set(all.map(p => `${p[0]},${p[1]}`));
|
|
115
|
+
if (u.size < 3) {
|
|
116
|
+
this.renderer.cancelDrawing();
|
|
117
|
+
this.stateMachine.forceTransitionTo(DrawState.IDLE);
|
|
118
|
+
this.currentDrawing = null;
|
|
119
|
+
this.clearMeasureLabels();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const feature = this.makeFeature(type, points, coord);
|
|
124
|
+
if (!feature) {
|
|
125
|
+
this.stateMachine.forceTransitionTo(DrawState.IDLE);
|
|
126
|
+
this.currentDrawing = null;
|
|
127
|
+
this.clearMeasureLabels();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (await this.invokeInsert(feature)) {
|
|
131
|
+
this.featureMap.set(feature.id, feature);
|
|
132
|
+
this.roundFeatureIds.add(feature.id);
|
|
133
|
+
this.renderer.cancelDrawing();
|
|
134
|
+
this.renderer.renderFeature(feature);
|
|
135
|
+
this.drawnCount++;
|
|
136
|
+
this.renderer.refreshAllFeatureLabels(this.getAllFeatures());
|
|
137
|
+
}
|
|
138
|
+
this.stateMachine.forceTransitionTo(DrawState.IDLE);
|
|
139
|
+
this.currentDrawing = null;
|
|
140
|
+
this.clearMeasureLabels();
|
|
141
|
+
this.log('finishDrawing', { type, id: feature.id, drawnCount: this.drawnCount });
|
|
142
|
+
}
|
|
143
|
+
startDraw(config) {
|
|
144
|
+
if (!this.stateMachine.isIdle())
|
|
145
|
+
throw new Error('Cannot start drawing while not in idle state');
|
|
146
|
+
this.config = { ...DEFAULT_DRAW_CONFIG, ...config, name: config.name || generateFeatureName() };
|
|
147
|
+
this.drawnCount = 0;
|
|
148
|
+
this.roundFeatureIds.clear();
|
|
149
|
+
this.log('startDraw', { type: this.config.type, limit: this.config.limit, enableEdit: this.config.enableEdit, enableDel: this.config.enableDel, oldFeatures: this.config.oldFeatures?.length || 0 });
|
|
150
|
+
this.renderer.setAllFeatureLabels(this.config.showLenForAll ?? false, this.config.showAreaForAll ?? false);
|
|
151
|
+
this.config.oldFeatures?.forEach(f => { this.featureMap.set(f.id, f); this.renderer.renderFeature(f); });
|
|
152
|
+
this.renderer.refreshAllFeatureLabels(this.getAllFeatures());
|
|
153
|
+
}
|
|
154
|
+
stopDraw() { this.destroyUi(); this.clearMeasureLabels(); this.stateMachine.forceTransitionTo(DrawState.IDLE); this.config = null; this.currentDrawing = null; this.drawnCount = 0; this.featureMap.clear(); this.roundFeatureIds.clear(); this.renderer.clearAll(); }
|
|
155
|
+
destroy() { this.destroyUi(); this.stopDraw(); this.clearMeasureLabels(); this.adapter.destroy(); }
|
|
156
|
+
getFeature(id) { return this.featureMap.get(id); }
|
|
157
|
+
;
|
|
158
|
+
getAllFeatures() { return Array.from(this.featureMap.values()); }
|
|
159
|
+
;
|
|
160
|
+
getState() { return this.stateMachine.state; }
|
|
161
|
+
updateFeatureInfo(f) { this.featureMap.set(f.id, f); }
|
|
162
|
+
canEditFeature(id) { return this.config?.enableEdit ?? false; }
|
|
163
|
+
canDeleteFeature(id) { return this.config?.enableDel ?? false; }
|
|
164
|
+
makeFeature(type, points, dblCoord) {
|
|
165
|
+
const name = this.config?.name || generateFeatureName();
|
|
166
|
+
const id = generateUniqueId();
|
|
167
|
+
switch (type) {
|
|
168
|
+
case 'rect': {
|
|
169
|
+
const [a, b] = points;
|
|
170
|
+
const c = dblCoord;
|
|
171
|
+
const ring = [[a[0], a[1]], [c[0], a[1]], [c[0], c[1]], [a[0], c[1]], [a[0], a[1]]];
|
|
172
|
+
return { id, type, wkt: toWktPolygon(ring), details: { ...DEFAULT_RECT_DETAILS, name } };
|
|
173
|
+
}
|
|
174
|
+
case 'triangle': {
|
|
175
|
+
const pts = points.length === 1 ? [...points, dblCoord, dblCoord] : [...points, dblCoord];
|
|
176
|
+
const ring = [pts[0], pts[1], pts[2], pts[0]];
|
|
177
|
+
return { id, type, wkt: toWktPolygon(ring), details: { ...DEFAULT_TRIANGLE_DETAILS, name } };
|
|
178
|
+
}
|
|
179
|
+
case 'circle': {
|
|
180
|
+
const center = points[0];
|
|
181
|
+
const edge = dblCoord;
|
|
182
|
+
const rDeg = Math.sqrt((edge[0] - center[0]) ** 2 + (edge[1] - center[1]) ** 2);
|
|
183
|
+
const rM = rDeg * metersPerDeg(center[1]);
|
|
184
|
+
const ring = buildCircleRing(center, rDeg, 64);
|
|
185
|
+
return { id, type, wkt: toWktPolygon(ring), details: { ...DEFAULT_CIRCLE_DETAILS, name, radius: Math.round(rM) } };
|
|
186
|
+
}
|
|
187
|
+
case 'line': {
|
|
188
|
+
const pts = [...points, dblCoord];
|
|
189
|
+
return { id, type, wkt: toWktLineString(pts), details: { ...DEFAULT_LINE_DETAILS, name } };
|
|
190
|
+
}
|
|
191
|
+
case 'polyline': {
|
|
192
|
+
const pts = [...points, dblCoord];
|
|
193
|
+
return { id, type, wkt: toWktLineString(pts), details: { ...DEFAULT_POLYLINE_DETAILS, name } };
|
|
194
|
+
}
|
|
195
|
+
case 'customShape': {
|
|
196
|
+
const pts = [...points, dblCoord];
|
|
197
|
+
const ring = [...pts, pts[0]];
|
|
198
|
+
return { id, type, wkt: toWktPolygon(ring), details: { ...DEFAULT_SHAPE_DETAILS, name } };
|
|
199
|
+
}
|
|
200
|
+
default: return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async invokeInsert(f) { return this.bizCallbacks.onInsert ? await Promise.resolve(this.bizCallbacks.onInsert(f)) : true; }
|
|
204
|
+
editFeature(id) { if (!this.stateMachine.isIdle())
|
|
205
|
+
return; const f = this.featureMap.get(id); if (!f)
|
|
206
|
+
return; this.stateMachine.transitionTo(DrawState.EDITING); this.editingFeature = f; this.renderer.startEditing(f); this.log('EDIT start', { id: f.id, type: f.type, details: f.details }); this.showEditPanel(f); }
|
|
207
|
+
requestDeleteFeature(id) { if (!this.stateMachine.isIdle())
|
|
208
|
+
return; const f = this.featureMap.get(id); if (!f)
|
|
209
|
+
return; this.log('DELETE request', { id: f.id, type: f.type }); this.showDeleteConfirm(f); }
|
|
210
|
+
async doDelete(id) { const f = this.featureMap.get(id); if (!f || !this.stateMachine.isIdle())
|
|
211
|
+
return; this.stateMachine.transitionTo(DrawState.DELETING); this.log('DELETE execute', { id: f.id }); const ok = this.bizCallbacks.onDelete ? await Promise.resolve(this.bizCallbacks.onDelete(f)) : true; if (ok) {
|
|
212
|
+
this.featureMap.delete(id);
|
|
213
|
+
this.roundFeatureIds.delete(id);
|
|
214
|
+
this.renderer.removeFeature(id);
|
|
215
|
+
this.log('DELETE done', { id: f.id });
|
|
216
|
+
}
|
|
217
|
+
else
|
|
218
|
+
this.log('DELETE fail', { id: f.id }); this.stateMachine.forceTransitionTo(DrawState.IDLE); }
|
|
219
|
+
// ─── Measure labels ────────────────────
|
|
220
|
+
updateMeasureLabels(type, points, coord) {
|
|
221
|
+
this.clearMeasureLabels();
|
|
222
|
+
if (!this.config)
|
|
223
|
+
return;
|
|
224
|
+
const sl = this.config.showLenOnInsert, sa = this.config.showAreaOnInsert;
|
|
225
|
+
if (!sl && !sa)
|
|
226
|
+
return;
|
|
227
|
+
const all = [...points, coord];
|
|
228
|
+
const cls = ['rect', 'triangle', 'circle', 'customShape'].includes(type);
|
|
229
|
+
const ctr = this.renderer.getContainer();
|
|
230
|
+
const L = (pos, txt, clr) => { const e = document.createElement('div'); e.style.cssText = `position:absolute;pointer-events:none;z-index:1500;color:${clr};font-size:12px;font-weight:600;white-space:nowrap;text-shadow:0 0 3px white,0 0 3px white;transform:translate(-50%,-50%);`; e.textContent = txt; const px = this.renderer.getPixelFromCoordinate(pos); e.style.left = `${px[0]}px`; e.style.top = `${px[1]}px`; ctr.appendChild(e); this.measureLabels.push(e); };
|
|
231
|
+
if (type === 'rect' && all.length >= 2) {
|
|
232
|
+
const [a, b] = all;
|
|
233
|
+
const w = geoDistance(a, [b[0], a[1]]), h = geoDistance(a, [a[0], b[1]]);
|
|
234
|
+
if (sl) {
|
|
235
|
+
L([(a[0] + b[0]) / 2, a[1]], formatMeters(w), '#2563eb');
|
|
236
|
+
L([a[0], (a[1] + b[1]) / 2], formatMeters(h), '#2563eb');
|
|
237
|
+
}
|
|
238
|
+
if (sa)
|
|
239
|
+
L([(a[0] + b[0]) / 2, (a[1] + b[1]) / 2], formatArea(w * h), '#dc2626');
|
|
240
|
+
}
|
|
241
|
+
else if (type === 'circle' && all.length >= 2) {
|
|
242
|
+
const r = geoDistance(all[0], all[1]);
|
|
243
|
+
const at = sa ? formatArea(Math.PI * r * r) : '';
|
|
244
|
+
const tx = at ? `r=${formatMeters(r)}, ${at}` : `r=${formatMeters(r)}`;
|
|
245
|
+
if (sl || sa)
|
|
246
|
+
L(all[0], tx, '#2563eb');
|
|
247
|
+
}
|
|
248
|
+
else if (cls && all.length >= 3) {
|
|
249
|
+
const ring = [...all, all[0]];
|
|
250
|
+
if (sl)
|
|
251
|
+
for (let i = 0; i < all.length; i++) {
|
|
252
|
+
const j = (i + 1) % all.length;
|
|
253
|
+
const d = geoDistance(all[i], all[j]);
|
|
254
|
+
if (d < 0.01)
|
|
255
|
+
continue;
|
|
256
|
+
L([(all[i][0] + all[j][0]) / 2, (all[i][1] + all[j][1]) / 2], formatMeters(d), '#2563eb');
|
|
257
|
+
}
|
|
258
|
+
if (sa)
|
|
259
|
+
L(geoPolygonCenter(all), formatArea(geoArea(all)), '#dc2626');
|
|
260
|
+
}
|
|
261
|
+
else if ((type === 'line' || type === 'polyline') && sl && all.length >= 2)
|
|
262
|
+
for (let i = 0; i < all.length - 1; i++) {
|
|
263
|
+
const d = geoDistance(all[i], all[i + 1]);
|
|
264
|
+
if (d < 0.01)
|
|
265
|
+
continue;
|
|
266
|
+
L([(all[i][0] + all[i + 1][0]) / 2, (all[i][1] + all[i + 1][1]) / 2], formatMeters(d), '#2563eb');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
clearMeasureLabels() { this.measureLabels.forEach(e => e.remove()); this.measureLabels = []; }
|
|
270
|
+
log(...args) { if (this.config?.debug)
|
|
271
|
+
console.log('[DrawTool]', ...args); }
|
|
272
|
+
showToast(msg) { const el = document.createElement('div'); el.style.cssText = 'position:fixed;top:250px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);color:#fff;padding:8px 20px;border-radius:6px;font-size:14px;z-index:3000;pointer-events:none;'; el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), 2000); }
|
|
273
|
+
// ─── UI panels ──────────────────────
|
|
274
|
+
showDeleteConfirm(feature) {
|
|
275
|
+
const self = this;
|
|
276
|
+
this.mountUi(onClose => () => h('div', { style: { ...panelBaseStyle, width: '360px', padding: '24px' } }, [
|
|
277
|
+
h('div', { style: { display: 'flex', gap: '12px', marginBottom: '20px' } }, [
|
|
278
|
+
h('div', { style: { width: '44px', height: '44px', borderRadius: '50%', backgroundColor: '#fef2f2', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 } }, [
|
|
279
|
+
h('svg', { viewBox: '0 0 24 24', width: '22', height: '22' }, [h('path', { d: 'M10 4h4v1h5v2H4V5h5V4ZM6 8h12l-.8 11.2a1 1 0 0 1-1 .8H7.8a1 1 0 0 1-1-.8L6 8Zm3 2v7h2v-7H9Zm4 0v7h2v-7h-2Z', fill: '#dc2626' })]),
|
|
280
|
+
]),
|
|
281
|
+
h('div', {}, [h('div', { style: { fontSize: '16px', fontWeight: '600', color: '#1f2937', marginBottom: '4px' } }, '确认删除'), h('div', { style: { fontSize: '13px', color: '#6b7280', lineHeight: '1.5' } }, '删除后将无法恢复,您确定要删除该要素吗?')])
|
|
282
|
+
]),
|
|
283
|
+
h('div', { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end' } }, [
|
|
284
|
+
h('button', { style: { ...btnBaseStyle, padding: '8px 20px', fontSize: '14px' }, onClick: onClose }, '取消'),
|
|
285
|
+
h('button', { style: { padding: '8px 20px', borderRadius: '4px', fontSize: '14px', cursor: 'pointer', backgroundColor: '#dc2626', color: '#fff', border: 'none' }, onClick: () => { onClose(); self.doDelete(feature.id); } }, '确认删除')
|
|
286
|
+
])
|
|
287
|
+
]), false);
|
|
288
|
+
}
|
|
289
|
+
showEditPanel(orig) {
|
|
290
|
+
const self = this;
|
|
291
|
+
const d = orig.details;
|
|
292
|
+
const hasFill = ['rect', 'triangle', 'circle', 'customShape'].includes(orig.type);
|
|
293
|
+
const hasRadius = orig.type === 'circle';
|
|
294
|
+
this.mountUi(onClose => {
|
|
295
|
+
const name = ref(d.name || ''), lineType = ref(d.lineType || 'solid'), lineRgba = ref(d.lineRgba || 'rgba(255,0,0,1)'), fillRgba = ref(d.fillRgba || 'rgba(255,0,0,0.5)'), textRgba = ref(d.textRgba || 'rgba(255,255,255,1)'), radius = ref(d.radius || 0), lineWidth = ref(d.lineWidth || 2), lineDash = ref(d.strokeLineDash || '15 15 15 15'), d1 = ref('15'), d2 = ref('15'), d3 = ref('15'), d4 = ref('15'), collapsed = ref(false);
|
|
296
|
+
const initDash = (lineDash.value || '15 15 15 15').split(/\s+/);
|
|
297
|
+
d1.value = initDash[0] || '15';
|
|
298
|
+
d2.value = initDash[1] || '15';
|
|
299
|
+
d3.value = initDash[2] || '15';
|
|
300
|
+
d4.value = initDash[3] || '15';
|
|
301
|
+
const syncDash = () => { lineDash.value = `${d1.value} ${d2.value} ${d3.value} ${d4.value}`; if (lineType.value === 'solid')
|
|
302
|
+
self.renderer.updateDashVariables(0, 0, 0, 0);
|
|
303
|
+
else
|
|
304
|
+
self.renderer.updateDashVariables(Number(d1.value), Number(d2.value), Number(d3.value), Number(d4.value)); };
|
|
305
|
+
if (lineType.value === 'solid')
|
|
306
|
+
self.renderer.updateDashVariables(0, 0, 0, 0);
|
|
307
|
+
else
|
|
308
|
+
self.renderer.updateDashVariables(Number(d1.value), Number(d2.value), Number(d3.value), Number(d4.value));
|
|
309
|
+
const deps = [lineRgba];
|
|
310
|
+
if (hasFill)
|
|
311
|
+
deps.push(fillRgba);
|
|
312
|
+
watch(deps, () => { self.renderer.updateFeatureStyle(orig.id, { strokeColor: lineRgba.value, fillColor: hasFill ? fillRgba.value : undefined, lineType: lineType.value }); }, { immediate: true });
|
|
313
|
+
watch(lineWidth, (v) => { self.renderer.updateFeatureStyle(orig.id, { strokeWidth: lineType.value === 'solid' ? v : v * 2 }); self.renderer.updateFeatureStyle(orig.id, { lineWidth: v }); }, { immediate: true });
|
|
314
|
+
self.renderer.onGeometryChange(() => { if (hasRadius) {
|
|
315
|
+
const rm = self.renderer.getEditFeatureRadius();
|
|
316
|
+
if (rm != null)
|
|
317
|
+
radius.value = Math.round(rm);
|
|
318
|
+
} });
|
|
319
|
+
const cp = (label, model) => h('div', { style: { marginBottom: '12px' } }, [h('label', { style: labelStyle }, label), h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px', marginBottom: '4px' } }, PRESET_COLORS.map(hex => h('span', { style: { width: '20px', height: '20px', borderRadius: '4px', border: '1px solid #d1d5db', cursor: 'pointer', backgroundColor: hex }, onClick: () => { model.value = rgbaFromHex(hex); } }))), h('div', { style: { display: 'flex', gap: '4px', alignItems: 'center' } }, [h('input', { type: 'color', style: { width: '32px', height: '32px', border: '1px solid #d1d5db', borderRadius: '4px', cursor: 'pointer', padding: '0' }, value: rgbaToHex(model.value), onInput: (e) => { model.value = rgbaFromHex(e.target.value); } }), h('input', { style: { flex: '1', border: '1px solid #d1d5db', borderRadius: '4px', padding: '4px 8px', fontSize: '12px' }, value: model.value, onInput: (e) => { model.value = e.target.value; } })])]);
|
|
320
|
+
const buildDetails = () => { const b = { name: name.value, lineWidth: lineWidth.value, lineType: lineType.value, strokeLineDash: lineDash.value, lineRgba: lineRgba.value, textRgba: textRgba.value }; if (hasFill)
|
|
321
|
+
b.fillRgba = fillRgba.value; if (hasRadius)
|
|
322
|
+
b.radius = radius.value; return b; };
|
|
323
|
+
return () => h('div', { style: { ...panelBaseStyle, width: '340px' } }, [h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 16px', borderBottom: collapsed.value ? 'none' : '1px solid #e5e7eb', backgroundColor: '#f9fafb', borderRadius: collapsed.value ? '8px' : '8px 8px 0 0', cursor: 'move', userSelect: 'none' }, onMousedown: (e) => self.dragPanel(e) }, [h('span', { style: { fontSize: '14px', fontWeight: '500', color: '#374151' } }, '要素编辑'), h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } }, [h('span', { style: { color: '#6b7280', cursor: 'pointer', fontSize: '18px', lineHeight: '1', userSelect: 'none', padding: '0 4px' }, onClick: () => { collapsed.value = !collapsed.value; } }, collapsed.value ? '+' : '\u2212'), h('span', { style: { color: '#9ca3af', cursor: 'pointer', fontSize: '18px', lineHeight: '1' }, onClick: onClose }, '\u00D7')])]), collapsed.value ? null : h('div', { style: { padding: '16px' } }, [h('div', { style: { marginBottom: '12px' } }, [h('label', { style: labelStyle }, '名称'), h('input', { style: inputStyle, value: name.value, onInput: (e) => { name.value = e.target.value; } })]), h('div', { style: { marginBottom: '12px' } }, [h('label', { style: labelStyle }, '线宽(px)'), h('input', { style: inputStyle, type: 'number', min: '1', max: '20', value: String(lineWidth.value), onInput: (e) => { lineWidth.value = Number(e.target.value); } })]), hasRadius ? h('div', { style: { marginBottom: '12px' } }, [h('label', { style: labelStyle }, '半径 (米)'), h('input', { style: inputStyle, type: 'number', value: String(radius.value), onInput: (e) => { radius.value = Number(e.target.value); } })]) : null, h('div', { style: { marginBottom: '12px' } }, [h('label', { style: labelStyle }, '线条类型'), h('select', { style: inputStyle, value: lineType.value, onChange: (e) => { lineType.value = e.target.value; syncDash(); } }, [h('option', { value: 'solid' }, '实线'), h('option', { value: 'dashed' }, '虚线')])]), lineType.value === 'dashed' ? h('div', { style: { marginBottom: '12px' } }, [h('label', { style: labelStyle }, '虚线间距'), h('div', { style: { display: 'flex', gap: '4px' } }, [h('input', { style: inputStyle, type: 'number', min: '1', value: d1.value, onInput: (e) => { d1.value = e.target.value; syncDash(); } }), h('input', { style: inputStyle, type: 'number', min: '1', value: d2.value, onInput: (e) => { d2.value = e.target.value; syncDash(); } }), h('input', { style: inputStyle, type: 'number', min: '1', value: d3.value, onInput: (e) => { d3.value = e.target.value; syncDash(); } }), h('input', { style: inputStyle, type: 'number', min: '1', value: d4.value, onInput: (e) => { d4.value = e.target.value; syncDash(); } })])]) : null, cp('线条颜色', lineRgba), hasFill ? cp('填充颜色', fillRgba) : null, cp('文本颜色', textRgba), h('div', { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '16px', paddingTop: '12px', borderTop: '1px solid #e5e7eb' } }, [h('button', { style: btnBaseStyle, onClick: onClose }, '关闭'), h('button', { style: { ...btnBaseStyle, backgroundColor: '#2563eb', color: '#fff', border: 'none' }, onClick: async () => { const updated = { ...orig, details: buildDetails() }; const ok = self.bizCallbacks.onEdit ? await Promise.resolve(self.bizCallbacks.onEdit(updated)) : true; onClose(); self.renderer.cancelEditing(); if (ok) {
|
|
324
|
+
self.featureMap.set(updated.id, updated);
|
|
325
|
+
self.renderer.updateFeatureStyle(updated.id, { fillColor: hasFill ? fillRgba.value : undefined, strokeColor: lineRgba.value, lineType: lineType.value });
|
|
326
|
+
if (orig.type === 'circle') {
|
|
327
|
+
const ci = updated.details;
|
|
328
|
+
const pts2 = parseWktLocal(orig.wkt);
|
|
329
|
+
if (pts2) {
|
|
330
|
+
const n2 = pts2.length - 1;
|
|
331
|
+
const cx = (pts2[0][0] + pts2[Math.floor(n2 / 2)][0]) / 2;
|
|
332
|
+
const cy = (pts2[0][1] + pts2[Math.floor(n2 / 2)][1]) / 2;
|
|
333
|
+
self.renderer.updateFeatureGeometry(updated.id, buildCircleRing([cx, cy], ci.radius / metersPerDeg(cy), 64));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} self.editingFeature = null; self.stateMachine.forceTransitionTo(DrawState.IDLE); } }, '保存')])])]);
|
|
337
|
+
}, true);
|
|
338
|
+
}
|
|
339
|
+
mountUi(setupFn, draggable) { this.destroyUi(); const self = this; const mountEl = document.createElement('div'); this.uiMountEl = mountEl; const onClose = () => { self.destroyUi(); if (self.stateMachine.isEditing() && self.editingFeature) {
|
|
340
|
+
self.renderer.cancelEditing();
|
|
341
|
+
self.editingFeature = null;
|
|
342
|
+
self.stateMachine.forceTransitionTo(DrawState.IDLE);
|
|
343
|
+
} }; const app = createApp({ setup() { const rf = setupFn(onClose); return () => h('div', { style: { pointerEvents: 'auto' }, onMousedown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation() }, [rf()]); } }); if (draggable) {
|
|
344
|
+
mountEl.style.cssText = 'position:absolute;z-index:2000;top:20px;left:20px;';
|
|
345
|
+
this.renderer.getContainer().appendChild(mountEl);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
mountEl.style.cssText = 'position:fixed;inset:0;z-index:2000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.15);';
|
|
349
|
+
document.body.appendChild(mountEl);
|
|
350
|
+
} ; app.mount(mountEl); this.uiApp = app; }
|
|
351
|
+
dragPanel(ev) { const el = this.uiMountEl; if (!el)
|
|
352
|
+
return; const sx = ev.clientX, sy = ev.clientY, sl = el.offsetLeft, st = el.offsetTop; const move = (e) => { el.style.left = `${sl + e.clientX - sx}px`; el.style.top = `${st + e.clientY - sy}px`; }; const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); }; document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); }
|
|
353
|
+
destroyUi() { this.uiApp?.unmount(); this.uiMountEl?.remove(); this.uiApp = null; this.uiMountEl = null; }
|
|
354
|
+
}
|
|
355
|
+
const panelBaseStyle = { backgroundColor: '#fff', borderRadius: '8px', boxShadow: '0 4px 16px rgba(0,0,0,0.15)' };
|
|
356
|
+
const labelStyle = { fontSize: '12px', color: '#6b7280', display: 'block', marginBottom: '4px' };
|
|
357
|
+
const inputStyle = { width: '100%', boxSizing: 'border-box', border: '1px solid #d1d5db', borderRadius: '4px', padding: '4px 8px', fontSize: '14px' };
|
|
358
|
+
const btnBaseStyle = { padding: '6px 16px', borderRadius: '4px', fontSize: '14px', cursor: 'pointer', border: '1px solid #d1d5db', backgroundColor: '#fff', color: '#4b5563' };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FeatureInfo, FeatureType } from '../types';
|
|
2
|
+
export interface FeatureStyle {
|
|
3
|
+
fillColor?: string;
|
|
4
|
+
strokeColor?: string;
|
|
5
|
+
strokeWidth?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface IFeatureRenderer {
|
|
8
|
+
startDrawing(type: FeatureType): void;
|
|
9
|
+
updatePreview(coordinates: [number, number][], type: FeatureType): void;
|
|
10
|
+
finishDrawing(): Promise<FeatureInfo>;
|
|
11
|
+
cancelDrawing(): void;
|
|
12
|
+
renderFeature(feature: FeatureInfo, isHighlight?: boolean): void;
|
|
13
|
+
removeFeature(featureId: number): void;
|
|
14
|
+
clearAll(): void;
|
|
15
|
+
startEditing(feature: FeatureInfo): void;
|
|
16
|
+
cancelEditing(): void;
|
|
17
|
+
showHoverActions(feature: FeatureInfo, position: [number, number], actions: HoverActionConfig): void;
|
|
18
|
+
hideHoverActions(): void;
|
|
19
|
+
getFeatureAtCoordinate(coordinate: [number, number]): FeatureInfo | null;
|
|
20
|
+
updateFeatureStyle(featureId: number, style: FeatureStyle): void;
|
|
21
|
+
updateFeatureGeometry(featureId: number, ring: [number, number][]): void;
|
|
22
|
+
getContainer(): HTMLElement;
|
|
23
|
+
onHoverAction(callback: (action: HoverActionType, feature: FeatureInfo) => void): void;
|
|
24
|
+
onGeometryChange(callback: () => void): void;
|
|
25
|
+
getEditFeatureRadius(): number | null;
|
|
26
|
+
updateDashVariables(d1: number, d2: number, d3: number, d4: number): void;
|
|
27
|
+
getPixelFromCoordinate(coord: [number, number]): [number, number];
|
|
28
|
+
setAllFeatureLabels(showLen: boolean, showArea: boolean): void;
|
|
29
|
+
refreshAllFeatureLabels(features: FeatureInfo[]): void;
|
|
30
|
+
highlightHoverFeature(featureId: number | null): void;
|
|
31
|
+
}
|
|
32
|
+
export type HoverActionType = 'edit' | 'delete';
|
|
33
|
+
export interface HoverActionConfig {
|
|
34
|
+
showEdit: boolean;
|
|
35
|
+
showDelete: boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface IGisAdapter {
|
|
38
|
+
getFeatureRenderer(): IFeatureRenderer;
|
|
39
|
+
destroy(): void;
|
|
40
|
+
}
|
|
41
|
+
export interface IBizCallbacks {
|
|
42
|
+
onInsert?: (info: FeatureInfo) => Promise<boolean> | boolean;
|
|
43
|
+
onEdit?: (info: FeatureInfo) => Promise<boolean> | boolean;
|
|
44
|
+
onDelete?: (info: FeatureInfo) => Promise<boolean> | boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface IToastHandler {
|
|
47
|
+
show(message: string): void;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { DrawTool } from './DrawTool';
|
|
2
|
+
export type { DrawToolOptions } from './DrawTool';
|
|
3
|
+
export { StateMachine } from './state-machine';
|
|
4
|
+
export { DrawState } from './types';
|
|
5
|
+
export type { FeatureInfo, FeatureType, RectInfo, TriangleInfo, CircleInfo, LineInfo, PolylineInfo, ShapeInfo } from './types';
|
|
6
|
+
export type { DrawConfig } from './types';
|
|
7
|
+
export { DEFAULT_DRAW_CONFIG, DEFAULT_RECT_DETAILS, DEFAULT_TRIANGLE_DETAILS, DEFAULT_CIRCLE_DETAILS, DEFAULT_LINE_DETAILS, DEFAULT_POLYLINE_DETAILS, DEFAULT_SHAPE_DETAILS, } from './types';
|
|
8
|
+
export { generateUniqueId, generateFeatureName, parseWkt, toWktPolygon, toWktLineString, isPointInPolygon, geoDistance, geoArea, geoPolygonCenter, formatMeters, formatArea } from './utils';
|
|
9
|
+
export type { IGisAdapter, IFeatureRenderer, FeatureStyle, IBizCallbacks, IToastHandler, HoverActionType, HoverActionConfig, } from './adapter';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { DrawTool } from './DrawTool';
|
|
2
|
+
export { StateMachine } from './state-machine';
|
|
3
|
+
export { DrawState } from './types';
|
|
4
|
+
export { DEFAULT_DRAW_CONFIG, DEFAULT_RECT_DETAILS, DEFAULT_TRIANGLE_DETAILS, DEFAULT_CIRCLE_DETAILS, DEFAULT_LINE_DETAILS, DEFAULT_POLYLINE_DETAILS, DEFAULT_SHAPE_DETAILS, } from './types';
|
|
5
|
+
export { generateUniqueId, generateFeatureName, parseWkt, toWktPolygon, toWktLineString, isPointInPolygon, geoDistance, geoArea, geoPolygonCenter, formatMeters, formatArea } from './utils';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DrawState } from '../types';
|
|
2
|
+
export declare class StateMachine {
|
|
3
|
+
private currentState;
|
|
4
|
+
get state(): DrawState;
|
|
5
|
+
canTransitionTo(targetState: DrawState): boolean;
|
|
6
|
+
transitionTo(targetState: DrawState): void;
|
|
7
|
+
forceTransitionTo(targetState: DrawState): void;
|
|
8
|
+
isIdle(): boolean;
|
|
9
|
+
isDrawing(): boolean;
|
|
10
|
+
isEditing(): boolean;
|
|
11
|
+
isDeleting(): boolean;
|
|
12
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { DrawState } from '../types';
|
|
2
|
+
export class StateMachine {
|
|
3
|
+
currentState = DrawState.IDLE;
|
|
4
|
+
get state() {
|
|
5
|
+
return this.currentState;
|
|
6
|
+
}
|
|
7
|
+
canTransitionTo(targetState) {
|
|
8
|
+
switch (this.currentState) {
|
|
9
|
+
case DrawState.IDLE:
|
|
10
|
+
return [DrawState.DRAWING, DrawState.EDITING, DrawState.DELETING].includes(targetState);
|
|
11
|
+
case DrawState.DRAWING:
|
|
12
|
+
return targetState === DrawState.IDLE;
|
|
13
|
+
case DrawState.EDITING:
|
|
14
|
+
return targetState === DrawState.IDLE;
|
|
15
|
+
case DrawState.DELETING:
|
|
16
|
+
return targetState === DrawState.IDLE;
|
|
17
|
+
default:
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
transitionTo(targetState) {
|
|
22
|
+
if (!this.canTransitionTo(targetState)) {
|
|
23
|
+
throw new Error(`Invalid state transition from ${this.currentState} to ${targetState}`);
|
|
24
|
+
}
|
|
25
|
+
this.currentState = targetState;
|
|
26
|
+
}
|
|
27
|
+
forceTransitionTo(targetState) {
|
|
28
|
+
this.currentState = targetState;
|
|
29
|
+
}
|
|
30
|
+
isIdle() {
|
|
31
|
+
return this.currentState === DrawState.IDLE;
|
|
32
|
+
}
|
|
33
|
+
isDrawing() {
|
|
34
|
+
return this.currentState === DrawState.DRAWING;
|
|
35
|
+
}
|
|
36
|
+
isEditing() {
|
|
37
|
+
return this.currentState === DrawState.EDITING;
|
|
38
|
+
}
|
|
39
|
+
isDeleting() {
|
|
40
|
+
return this.currentState === DrawState.DELETING;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { FeatureType, FeatureInfo } from './feature';
|
|
2
|
+
export interface DrawConfig {
|
|
3
|
+
type: FeatureType;
|
|
4
|
+
name?: string;
|
|
5
|
+
limit?: number;
|
|
6
|
+
enableLimitMsg?: boolean;
|
|
7
|
+
enableEdit?: boolean;
|
|
8
|
+
enableDel?: boolean;
|
|
9
|
+
showLenOnInsert?: boolean;
|
|
10
|
+
showAreaOnInsert?: boolean;
|
|
11
|
+
showLenForAll?: boolean;
|
|
12
|
+
showAreaForAll?: boolean;
|
|
13
|
+
debug?: boolean;
|
|
14
|
+
oldFeatures?: FeatureInfo[];
|
|
15
|
+
}
|
|
16
|
+
export declare const DEFAULT_DRAW_CONFIG: Omit<DrawConfig, 'type'>;
|
|
17
|
+
export declare const DEFAULT_RECT_DETAILS: {
|
|
18
|
+
name: string;
|
|
19
|
+
lineWidth: number;
|
|
20
|
+
lineType: "solid";
|
|
21
|
+
strokeLineDash: string;
|
|
22
|
+
lineRgba: string;
|
|
23
|
+
fillRgba: string;
|
|
24
|
+
textRgba: string;
|
|
25
|
+
};
|
|
26
|
+
export declare const DEFAULT_TRIANGLE_DETAILS: {
|
|
27
|
+
name: string;
|
|
28
|
+
lineWidth: number;
|
|
29
|
+
lineType: "solid";
|
|
30
|
+
strokeLineDash: string;
|
|
31
|
+
lineRgba: string;
|
|
32
|
+
fillRgba: string;
|
|
33
|
+
textRgba: string;
|
|
34
|
+
};
|
|
35
|
+
export declare const DEFAULT_CIRCLE_DETAILS: {
|
|
36
|
+
name: string;
|
|
37
|
+
radius: number;
|
|
38
|
+
lineWidth: number;
|
|
39
|
+
lineType: "solid";
|
|
40
|
+
strokeLineDash: string;
|
|
41
|
+
lineRgba: string;
|
|
42
|
+
fillRgba: string;
|
|
43
|
+
textRgba: string;
|
|
44
|
+
};
|
|
45
|
+
export declare const DEFAULT_LINE_DETAILS: {
|
|
46
|
+
name: string;
|
|
47
|
+
lineWidth: number;
|
|
48
|
+
lineType: "solid";
|
|
49
|
+
strokeLineDash: string;
|
|
50
|
+
lineRgba: string;
|
|
51
|
+
textRgba: string;
|
|
52
|
+
};
|
|
53
|
+
export declare const DEFAULT_POLYLINE_DETAILS: {
|
|
54
|
+
name: string;
|
|
55
|
+
lineWidth: number;
|
|
56
|
+
lineType: "solid";
|
|
57
|
+
strokeLineDash: string;
|
|
58
|
+
lineRgba: string;
|
|
59
|
+
textRgba: string;
|
|
60
|
+
};
|
|
61
|
+
export declare const DEFAULT_SHAPE_DETAILS: {
|
|
62
|
+
name: string;
|
|
63
|
+
lineWidth: number;
|
|
64
|
+
lineType: "solid";
|
|
65
|
+
strokeLineDash: string;
|
|
66
|
+
lineRgba: string;
|
|
67
|
+
fillRgba: string;
|
|
68
|
+
textRgba: string;
|
|
69
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const DEFAULT_DRAW_CONFIG = { limit: 1, enableLimitMsg: false, enableEdit: false, enableDel: false, showLenOnInsert: false, showAreaOnInsert: false, showLenForAll: false, showAreaForAll: false, debug: false };
|
|
2
|
+
export const DEFAULT_RECT_DETAILS = { name: '', lineWidth: 2, lineType: 'solid', strokeLineDash: '15 15 15 15', lineRgba: 'rgba(255,0,0,1)', fillRgba: 'rgba(255,0,0,0.5)', textRgba: 'rgba(255,255,255,1)' };
|
|
3
|
+
export const DEFAULT_TRIANGLE_DETAILS = { name: '', lineWidth: 2, lineType: 'solid', strokeLineDash: '15 15 15 15', lineRgba: 'rgba(0,0,255,1)', fillRgba: 'rgba(0,0,255,0.5)', textRgba: 'rgba(255,255,255,1)' };
|
|
4
|
+
export const DEFAULT_CIRCLE_DETAILS = { name: '', radius: 0, lineWidth: 2, lineType: 'solid', strokeLineDash: '15 15 15 15', lineRgba: 'rgba(34,197,94,1)', fillRgba: 'rgba(34,197,94,0.5)', textRgba: 'rgba(255,255,255,1)' };
|
|
5
|
+
export const DEFAULT_LINE_DETAILS = { name: '', lineWidth: 2, lineType: 'solid', strokeLineDash: '15 15 15 15', lineRgba: 'rgba(234,179,8,1)', textRgba: 'rgba(255,255,255,1)' };
|
|
6
|
+
export const DEFAULT_POLYLINE_DETAILS = { name: '', lineWidth: 2, lineType: 'solid', strokeLineDash: '15 15 15 15', lineRgba: 'rgba(234,179,8,1)', textRgba: 'rgba(255,255,255,1)' };
|
|
7
|
+
export const DEFAULT_SHAPE_DETAILS = { name: '', lineWidth: 2, lineType: 'solid', strokeLineDash: '15 15 15 15', lineRgba: 'rgba(139,92,246,1)', fillRgba: 'rgba(139,92,246,0.5)', textRgba: 'rgba(255,255,255,1)' };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface FeatureInfo {
|
|
2
|
+
id: number;
|
|
3
|
+
type: FeatureType;
|
|
4
|
+
wkt: string;
|
|
5
|
+
details: RectInfo | TriangleInfo | CircleInfo | LineInfo | PolylineInfo | ShapeInfo;
|
|
6
|
+
}
|
|
7
|
+
export type FeatureType = 'rect' | 'triangle' | 'circle' | 'line' | 'polyline' | 'customShape';
|
|
8
|
+
export interface RectInfo {
|
|
9
|
+
name: string;
|
|
10
|
+
lineWidth: number;
|
|
11
|
+
lineType: 'dashed' | 'solid';
|
|
12
|
+
strokeLineDash: string;
|
|
13
|
+
lineRgba: string;
|
|
14
|
+
fillRgba: string;
|
|
15
|
+
textRgba: string;
|
|
16
|
+
}
|
|
17
|
+
export interface TriangleInfo {
|
|
18
|
+
name: string;
|
|
19
|
+
lineWidth: number;
|
|
20
|
+
lineType: 'dashed' | 'solid';
|
|
21
|
+
strokeLineDash: string;
|
|
22
|
+
lineRgba: string;
|
|
23
|
+
fillRgba: string;
|
|
24
|
+
textRgba: string;
|
|
25
|
+
}
|
|
26
|
+
export interface CircleInfo {
|
|
27
|
+
name: string;
|
|
28
|
+
radius: number;
|
|
29
|
+
lineWidth: number;
|
|
30
|
+
lineType: 'dashed' | 'solid';
|
|
31
|
+
strokeLineDash: string;
|
|
32
|
+
lineRgba: string;
|
|
33
|
+
fillRgba: string;
|
|
34
|
+
textRgba: string;
|
|
35
|
+
}
|
|
36
|
+
export interface LineInfo {
|
|
37
|
+
name: string;
|
|
38
|
+
lineWidth: number;
|
|
39
|
+
lineType: 'dashed' | 'solid';
|
|
40
|
+
strokeLineDash: string;
|
|
41
|
+
lineRgba: string;
|
|
42
|
+
textRgba: string;
|
|
43
|
+
}
|
|
44
|
+
export interface PolylineInfo {
|
|
45
|
+
name: string;
|
|
46
|
+
lineWidth: number;
|
|
47
|
+
lineType: 'dashed' | 'solid';
|
|
48
|
+
strokeLineDash: string;
|
|
49
|
+
lineRgba: string;
|
|
50
|
+
textRgba: string;
|
|
51
|
+
}
|
|
52
|
+
export interface ShapeInfo {
|
|
53
|
+
name: string;
|
|
54
|
+
lineWidth: number;
|
|
55
|
+
lineType: 'dashed' | 'solid';
|
|
56
|
+
strokeLineDash: string;
|
|
57
|
+
lineRgba: string;
|
|
58
|
+
fillRgba: string;
|
|
59
|
+
textRgba: string;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { FeatureInfo, FeatureType, RectInfo, TriangleInfo, CircleInfo, LineInfo, PolylineInfo, ShapeInfo } from './feature';
|
|
2
|
+
export type { DrawConfig } from './config';
|
|
3
|
+
export { DEFAULT_DRAW_CONFIG, DEFAULT_RECT_DETAILS, DEFAULT_TRIANGLE_DETAILS, DEFAULT_CIRCLE_DETAILS, DEFAULT_LINE_DETAILS, DEFAULT_POLYLINE_DETAILS, DEFAULT_SHAPE_DETAILS, } from './config';
|
|
4
|
+
export { DrawState } from './state';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function generateUniqueId(): number;
|
|
2
|
+
export declare function generateFeatureName(length?: number): string;
|
|
3
|
+
export declare function isPointInPolygon(point: [number, number], polygon: [number, number][]): boolean;
|
|
4
|
+
export declare function parseWkt(wkt: string): [number, number][];
|
|
5
|
+
export declare function toWktPolygon(coordinates: [number, number][]): string;
|
|
6
|
+
export declare function toWktLineString(coordinates: [number, number][]): string;
|
|
7
|
+
export declare function geoDistance(a: [number, number], b: [number, number]): number;
|
|
8
|
+
export declare function geoArea(coords: [number, number][]): number;
|
|
9
|
+
export declare function geoPolygonCenter(coords: [number, number][]): [number, number];
|
|
10
|
+
export declare function formatMeters(m: number): string;
|
|
11
|
+
export declare function formatArea(area: number): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
let counter = 0;
|
|
2
|
+
export function generateUniqueId() {
|
|
3
|
+
counter += 1;
|
|
4
|
+
return Date.now() * 100 + counter;
|
|
5
|
+
}
|
|
6
|
+
export function generateFeatureName(length = 8) {
|
|
7
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
8
|
+
let result = '';
|
|
9
|
+
for (let i = 0; i < length; i++) {
|
|
10
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
export function isPointInPolygon(point, polygon) {
|
|
15
|
+
let inside = false;
|
|
16
|
+
const [px, py] = point;
|
|
17
|
+
let j = polygon.length - 1;
|
|
18
|
+
for (let i = 0; i < polygon.length; i++) {
|
|
19
|
+
const [xi, yi] = polygon[i];
|
|
20
|
+
const [xj, yj] = polygon[j];
|
|
21
|
+
if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) {
|
|
22
|
+
inside = !inside;
|
|
23
|
+
}
|
|
24
|
+
j = i;
|
|
25
|
+
}
|
|
26
|
+
return inside;
|
|
27
|
+
}
|
|
28
|
+
export function parseWkt(wkt) {
|
|
29
|
+
const m = wkt.match(/\((.*)\)/);
|
|
30
|
+
if (!m)
|
|
31
|
+
throw new Error(`Invalid WKT: ${wkt}`);
|
|
32
|
+
let inner = m[1];
|
|
33
|
+
inner = inner.replace(/^\(/, '').replace(/\)$/, '');
|
|
34
|
+
return inner.split(',').map((pair) => {
|
|
35
|
+
const [x, y] = pair.trim().split(/\s+/).map(Number);
|
|
36
|
+
return [x, y];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function toWktPolygon(coordinates) {
|
|
40
|
+
const coords = coordinates.map(([x, y]) => `${x} ${y}`).join(', ');
|
|
41
|
+
return `POLYGON((${coords}))`;
|
|
42
|
+
}
|
|
43
|
+
export function toWktLineString(coordinates) {
|
|
44
|
+
const coords = coordinates.map(([x, y]) => `${x} ${y}`).join(', ');
|
|
45
|
+
return `LINESTRING(${coords})`;
|
|
46
|
+
}
|
|
47
|
+
export function geoDistance(a, b) {
|
|
48
|
+
const R = 6371000;
|
|
49
|
+
const dLat = (b[1] - a[1]) * Math.PI / 180;
|
|
50
|
+
const dLon = (b[0] - a[0]) * Math.PI / 180;
|
|
51
|
+
const lat1 = a[1] * Math.PI / 180;
|
|
52
|
+
const lat2 = b[1] * Math.PI / 180;
|
|
53
|
+
const sinDLat = Math.sin(dLat / 2);
|
|
54
|
+
const sinDLon = Math.sin(dLon / 2);
|
|
55
|
+
const h = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon;
|
|
56
|
+
return 2 * R * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
|
|
57
|
+
}
|
|
58
|
+
export function geoArea(coords) {
|
|
59
|
+
const n = coords.length;
|
|
60
|
+
if (n < 3)
|
|
61
|
+
return 0;
|
|
62
|
+
let area = 0;
|
|
63
|
+
for (let i = 0; i < n; i++) {
|
|
64
|
+
const j = (i + 1) % n;
|
|
65
|
+
area += coords[i][0] * coords[j][1];
|
|
66
|
+
area -= coords[j][0] * coords[i][1];
|
|
67
|
+
}
|
|
68
|
+
area = Math.abs(area) / 2;
|
|
69
|
+
const centerLat = coords.reduce((s, c) => s + c[1], 0) / n;
|
|
70
|
+
const latRad = centerLat * Math.PI / 180;
|
|
71
|
+
const mPerDegLon = 111320 * Math.cos(latRad);
|
|
72
|
+
const mPerDegLat = 111320;
|
|
73
|
+
return area * mPerDegLon * mPerDegLat;
|
|
74
|
+
}
|
|
75
|
+
export function geoPolygonCenter(coords) {
|
|
76
|
+
const n = coords.length;
|
|
77
|
+
let cx = 0, cy = 0;
|
|
78
|
+
for (const c of coords) {
|
|
79
|
+
cx += c[0];
|
|
80
|
+
cy += c[1];
|
|
81
|
+
}
|
|
82
|
+
return [cx / n, cy / n];
|
|
83
|
+
}
|
|
84
|
+
export function formatMeters(m) {
|
|
85
|
+
if (m < 0.01)
|
|
86
|
+
return '0.00米';
|
|
87
|
+
if (m < 1)
|
|
88
|
+
return m.toFixed(2) + '米';
|
|
89
|
+
if (m < 1000)
|
|
90
|
+
return m.toFixed(1) + '米';
|
|
91
|
+
return (m / 1000).toFixed(2) + '千米';
|
|
92
|
+
}
|
|
93
|
+
export function formatArea(area) {
|
|
94
|
+
if (area < 0.01)
|
|
95
|
+
return '0.00㎡';
|
|
96
|
+
if (area < 1)
|
|
97
|
+
return area.toFixed(2) + '㎡';
|
|
98
|
+
if (area < 10000)
|
|
99
|
+
return area.toFixed(1) + '㎡';
|
|
100
|
+
return (area / 1000000).toFixed(2) + 'k㎡';
|
|
101
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@loongbao-web-gis-utils/draw-utils-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "loongbao",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"vue": "^3.5.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.5.0",
|
|
24
|
+
"vue": "^3.5.0"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"clean": "node -e \"const{rmSync}=require('fs');rmSync('dist',{recursive:true,force:true})\""
|
|
29
|
+
}
|
|
30
|
+
}
|