@mongoosejs/studio 0.2.5 → 0.2.7
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/backend/actions/ChatMessage/executeScript.js +2 -1
- package/backend/actions/Model/createChatMessage.js +54 -0
- package/backend/actions/Model/index.js +2 -0
- package/backend/actions/Model/streamChatMessage.js +58 -0
- package/backend/authorize.js +1 -0
- package/backend/helpers/getModelDescriptions.js +8 -0
- package/backend/integrations/callLLM.js +1 -1
- package/backend/integrations/streamLLM.js +1 -1
- package/docs/user_stories.md +13 -0
- package/frontend/public/app.js +20149 -16871
- package/frontend/public/tw.css +95 -0
- package/frontend/src/api.js +60 -0
- package/frontend/src/chat/chat.js +6 -2
- package/frontend/src/create-document/create-document.html +36 -1
- package/frontend/src/create-document/create-document.js +51 -1
- package/frontend/src/detail-default/detail-default.html +15 -2
- package/frontend/src/detail-default/detail-default.js +1066 -2
- package/frontend/src/document-details/document-property/document-property.html +71 -3
- package/frontend/src/document-details/document-property/document-property.js +67 -1
- package/frontend/src/models/models.html +41 -3
- package/frontend/src/models/models.js +252 -3
- package/package.json +3 -2
|
@@ -1,9 +1,62 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/* global L */
|
|
3
4
|
const template = require('./detail-default.html');
|
|
5
|
+
const appendCSS = require('../appendCSS');
|
|
6
|
+
|
|
7
|
+
// Add CSS for polygon vertex markers and context menu
|
|
8
|
+
appendCSS(`
|
|
9
|
+
.polygon-vertex-marker {
|
|
10
|
+
pointer-events: auto !important;
|
|
11
|
+
}
|
|
12
|
+
.polygon-vertex-marker > div {
|
|
13
|
+
pointer-events: auto !important;
|
|
14
|
+
}
|
|
15
|
+
.leaflet-context-menu {
|
|
16
|
+
position: absolute;
|
|
17
|
+
background: white;
|
|
18
|
+
border: 1px solid #ccc;
|
|
19
|
+
border-radius: 4px;
|
|
20
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
21
|
+
z-index: 10000;
|
|
22
|
+
min-width: 120px;
|
|
23
|
+
padding: 4px 0;
|
|
24
|
+
}
|
|
25
|
+
.leaflet-context-menu-item {
|
|
26
|
+
padding: 8px 16px;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
font-size: 14px;
|
|
29
|
+
color: #333;
|
|
30
|
+
}
|
|
31
|
+
.leaflet-context-menu-item:hover {
|
|
32
|
+
background-color: #f0f0f0;
|
|
33
|
+
}
|
|
34
|
+
.leaflet-context-menu-item.delete {
|
|
35
|
+
color: #dc3545;
|
|
36
|
+
}
|
|
37
|
+
.leaflet-context-menu-item.delete:hover {
|
|
38
|
+
background-color: #fee;
|
|
39
|
+
}
|
|
40
|
+
`);
|
|
41
|
+
|
|
4
42
|
module.exports = app => app.component('detail-default', {
|
|
5
43
|
template: template,
|
|
6
|
-
props: ['value'],
|
|
44
|
+
props: ['value', 'viewMode', 'onChange'],
|
|
45
|
+
data() {
|
|
46
|
+
return {
|
|
47
|
+
mapVisible: false,
|
|
48
|
+
mapInstance: null,
|
|
49
|
+
mapLayer: null,
|
|
50
|
+
draggableMarker: null,
|
|
51
|
+
draggableMarkers: [], // For polygon vertices
|
|
52
|
+
hasUnsavedChanges: false,
|
|
53
|
+
currentEditedGeometry: null, // Track the current edited geometry state
|
|
54
|
+
contextMenu: null, // Custom context menu element
|
|
55
|
+
contextMenuMarker: null, // Marker that triggered context menu
|
|
56
|
+
originalGeometry: null, // Store the original geometry when editing starts
|
|
57
|
+
isCreatingMarkers: false // Guard against re-entrant marker creation
|
|
58
|
+
};
|
|
59
|
+
},
|
|
7
60
|
computed: {
|
|
8
61
|
displayValue() {
|
|
9
62
|
if (this.value === null) {
|
|
@@ -23,6 +76,1017 @@ module.exports = app => app.component('detail-default', {
|
|
|
23
76
|
} catch (err) {
|
|
24
77
|
return String(this.value);
|
|
25
78
|
}
|
|
79
|
+
},
|
|
80
|
+
isGeoJsonGeometry() {
|
|
81
|
+
return this.value != null
|
|
82
|
+
&& typeof this.value === 'object'
|
|
83
|
+
&& !Array.isArray(this.value)
|
|
84
|
+
&& Object.prototype.hasOwnProperty.call(this.value, 'type')
|
|
85
|
+
&& Object.prototype.hasOwnProperty.call(this.value, 'coordinates');
|
|
86
|
+
},
|
|
87
|
+
isGeoJsonPoint() {
|
|
88
|
+
return this.isGeoJsonGeometry && this.value.type === 'Point';
|
|
89
|
+
},
|
|
90
|
+
isGeoJsonPolygon() {
|
|
91
|
+
return this.isGeoJsonGeometry && (this.value.type === 'Polygon' || this.value.type === 'MultiPolygon');
|
|
92
|
+
},
|
|
93
|
+
isMultiPolygon() {
|
|
94
|
+
return this.isGeoJsonGeometry && this.value.type === 'MultiPolygon';
|
|
95
|
+
},
|
|
96
|
+
isEditable() {
|
|
97
|
+
return (this.isGeoJsonPoint || this.isGeoJsonPolygon) && typeof this.onChange === 'function';
|
|
98
|
+
},
|
|
99
|
+
canUndo() {
|
|
100
|
+
// Can undo if there are any changes from the original geometry
|
|
101
|
+
return this.hasUnsavedChanges && this.originalGeometry != null;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
watch: {
|
|
105
|
+
viewMode: {
|
|
106
|
+
handler(newValue) {
|
|
107
|
+
this.mapVisible = newValue === 'map';
|
|
108
|
+
if (this.mapVisible) {
|
|
109
|
+
this.$nextTick(() => {
|
|
110
|
+
this.ensureMap();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
immediate: true
|
|
115
|
+
},
|
|
116
|
+
value: {
|
|
117
|
+
handler(newValue) {
|
|
118
|
+
if (this.mapVisible) {
|
|
119
|
+
this.$nextTick(() => {
|
|
120
|
+
this.ensureMap();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// Reset unsaved changes flag when value changes externally (e.g., after save)
|
|
124
|
+
if (this.hasUnsavedChanges && (this.isGeoJsonPoint || this.isGeoJsonPolygon)) {
|
|
125
|
+
this.hasUnsavedChanges = false;
|
|
126
|
+
this.currentEditedGeometry = null; // Reset edited geometry when value changes externally
|
|
127
|
+
}
|
|
128
|
+
// Store the new value as the original geometry for future edits
|
|
129
|
+
if (newValue && this.isGeoJsonGeometry) {
|
|
130
|
+
this.originalGeometry = JSON.parse(JSON.stringify(newValue));
|
|
131
|
+
} else {
|
|
132
|
+
this.originalGeometry = null;
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
deep: true,
|
|
136
|
+
immediate: true
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
beforeDestroy() {
|
|
140
|
+
this.hideContextMenu();
|
|
141
|
+
if (this.draggableMarker) {
|
|
142
|
+
this.draggableMarker.remove();
|
|
143
|
+
this.draggableMarker = null;
|
|
144
|
+
}
|
|
145
|
+
// Clean up polygon vertex markers
|
|
146
|
+
this.draggableMarkers.forEach(marker => {
|
|
147
|
+
if (marker) {
|
|
148
|
+
marker.remove();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
this.draggableMarkers = [];
|
|
152
|
+
if (this.mapInstance) {
|
|
153
|
+
this.mapInstance.remove();
|
|
154
|
+
this.mapInstance = null;
|
|
155
|
+
this.mapLayer = null;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
methods: {
|
|
159
|
+
ensureMap() {
|
|
160
|
+
if (!this.mapVisible || !this.isGeoJsonGeometry || !this.$refs.map) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof L === 'undefined') {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!this.mapInstance) {
|
|
169
|
+
// Ensure the map container has explicit dimensions
|
|
170
|
+
const mapElement = this.$refs.map;
|
|
171
|
+
if (mapElement) {
|
|
172
|
+
// Set explicit dimensions inline with !important to override any CSS
|
|
173
|
+
mapElement.style.setProperty('height', '256px', 'important');
|
|
174
|
+
mapElement.style.setProperty('min-height', '256px', 'important');
|
|
175
|
+
mapElement.style.setProperty('width', '100%', 'important');
|
|
176
|
+
mapElement.style.setProperty('display', 'block', 'important');
|
|
177
|
+
|
|
178
|
+
// Force a reflow to ensure dimensions are applied
|
|
179
|
+
void mapElement.offsetHeight;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
this.mapInstance = L.map(this.$refs.map, {
|
|
184
|
+
preferCanvas: false
|
|
185
|
+
}).setView([0, 0], 1);
|
|
186
|
+
|
|
187
|
+
// Ensure map container has relative positioning for context menu
|
|
188
|
+
const mapContainer = this.mapInstance.getContainer();
|
|
189
|
+
if (mapContainer) {
|
|
190
|
+
mapContainer.style.position = 'relative';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
194
|
+
attribution: '© OpenStreetMap contributors'
|
|
195
|
+
}).addTo(this.mapInstance);
|
|
196
|
+
|
|
197
|
+
// Explicitly invalidate size after creation to ensure proper rendering
|
|
198
|
+
this.$nextTick(() => {
|
|
199
|
+
if (this.mapInstance) {
|
|
200
|
+
this.mapInstance.invalidateSize();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.updateMapLayer();
|
|
209
|
+
this.$nextTick(() => {
|
|
210
|
+
if (this.mapInstance) {
|
|
211
|
+
this.mapInstance.invalidateSize();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
updateMapLayer() {
|
|
216
|
+
if (!this.mapInstance || !this.isGeoJsonGeometry) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// For Point geometries in edit mode, use a draggable marker
|
|
221
|
+
if (this.isGeoJsonPoint && this.isEditable) {
|
|
222
|
+
const [lng, lat] = this.value.coordinates;
|
|
223
|
+
|
|
224
|
+
// If marker exists, update its position instead of recreating
|
|
225
|
+
if (this.draggableMarker) {
|
|
226
|
+
const currentLatLng = this.draggableMarker.getLatLng();
|
|
227
|
+
// Only update if coordinates actually changed (avoid interrupting drag)
|
|
228
|
+
if (Math.abs(currentLatLng.lat - lat) > 0.0001 || Math.abs(currentLatLng.lng - lng) > 0.0001) {
|
|
229
|
+
this.draggableMarker.setLatLng([lat, lng]);
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Create new draggable marker
|
|
233
|
+
this.draggableMarker = L.marker([lat, lng], {
|
|
234
|
+
draggable: true
|
|
235
|
+
}).addTo(this.mapInstance);
|
|
236
|
+
|
|
237
|
+
// Add dragend event handler
|
|
238
|
+
this.draggableMarker.on('dragend', () => {
|
|
239
|
+
const newLat = this.draggableMarker.getLatLng().lat;
|
|
240
|
+
const newLng = this.draggableMarker.getLatLng().lng;
|
|
241
|
+
const newGeometry = {
|
|
242
|
+
type: 'Point',
|
|
243
|
+
coordinates: [newLng, newLat]
|
|
244
|
+
};
|
|
245
|
+
this.hasUnsavedChanges = true;
|
|
246
|
+
if (this.onChange) {
|
|
247
|
+
this.onChange(newGeometry);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Center map on marker with appropriate zoom level
|
|
252
|
+
const currentZoom = this.mapInstance.getZoom();
|
|
253
|
+
// If zoom is too low (less than 10), set a good default zoom level (13)
|
|
254
|
+
const targetZoom = currentZoom < 10 ? 13 : currentZoom;
|
|
255
|
+
this.mapInstance.setView([lat, lng], targetZoom);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Remove any existing non-draggable layer
|
|
259
|
+
if (this.mapLayer) {
|
|
260
|
+
this.mapLayer.remove();
|
|
261
|
+
this.mapLayer = null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Clean up polygon markers if they exist
|
|
265
|
+
this.draggableMarkers.forEach(marker => {
|
|
266
|
+
if (marker) marker.remove();
|
|
267
|
+
});
|
|
268
|
+
this.draggableMarkers = [];
|
|
269
|
+
} else if (this.isGeoJsonPolygon && this.isEditable) {
|
|
270
|
+
// Initialize current edited geometry if not set
|
|
271
|
+
if (!this.currentEditedGeometry) {
|
|
272
|
+
this.currentEditedGeometry = JSON.parse(JSON.stringify(this.value));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// For Polygon geometries in edit mode, create polygon layer with draggable vertex markers
|
|
276
|
+
// Use current edited geometry if available, otherwise use value
|
|
277
|
+
const polygonGeometryToUse = this.currentEditedGeometry || this.value;
|
|
278
|
+
const feature = {
|
|
279
|
+
type: 'Feature',
|
|
280
|
+
geometry: polygonGeometryToUse,
|
|
281
|
+
properties: {}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Update or create polygon layer
|
|
285
|
+
if (this.mapLayer) {
|
|
286
|
+
this.mapLayer.remove();
|
|
287
|
+
}
|
|
288
|
+
this.mapLayer = L.geoJSON(feature, {
|
|
289
|
+
style: {
|
|
290
|
+
color: '#3388ff',
|
|
291
|
+
weight: 2,
|
|
292
|
+
opacity: 0.8,
|
|
293
|
+
fillOpacity: 0.2
|
|
294
|
+
},
|
|
295
|
+
interactive: this.isMultiPolygon // Only interactive for MultiPolygon (to allow edge clicks)
|
|
296
|
+
}).addTo(this.mapInstance);
|
|
297
|
+
|
|
298
|
+
// Add contextmenu handler to polygon edges to add vertices (only for MultiPolygon)
|
|
299
|
+
if (this.isMultiPolygon) {
|
|
300
|
+
this.mapLayer.eachLayer((layer) => {
|
|
301
|
+
layer.on('contextmenu', (e) => {
|
|
302
|
+
e.originalEvent.preventDefault();
|
|
303
|
+
e.originalEvent.stopPropagation();
|
|
304
|
+
|
|
305
|
+
// Check if clicking near an existing marker (using pixel distance)
|
|
306
|
+
const clickPoint = e.latlng;
|
|
307
|
+
const clickContainerPoint = this.mapInstance.latLngToContainerPoint(clickPoint);
|
|
308
|
+
|
|
309
|
+
const isClickingOnMarker = this.draggableMarkers.some(marker => {
|
|
310
|
+
if (!marker || !marker._icon) return false;
|
|
311
|
+
const markerLatLng = marker.getLatLng();
|
|
312
|
+
const markerContainerPoint = this.mapInstance.latLngToContainerPoint(markerLatLng);
|
|
313
|
+
|
|
314
|
+
// Calculate pixel distance
|
|
315
|
+
const dx = clickContainerPoint.x - markerContainerPoint.x;
|
|
316
|
+
const dy = clickContainerPoint.y - markerContainerPoint.y;
|
|
317
|
+
const pixelDistance = Math.sqrt(dx * dx + dy * dy);
|
|
318
|
+
|
|
319
|
+
// 15 pixel threshold - marker icon is about 12px, so 15px gives some buffer
|
|
320
|
+
return pixelDistance < 15;
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!isClickingOnMarker) {
|
|
324
|
+
this.showAddVertexContextMenu(e.originalEvent, clickPoint);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Make polygon layers non-interactive to avoid interfering with marker dragging
|
|
331
|
+
this.mapLayer.eachLayer((layer) => {
|
|
332
|
+
layer.setStyle({
|
|
333
|
+
interactive: this.isMultiPolygon, // Only interactive for MultiPolygon (for context menu)
|
|
334
|
+
stroke: true,
|
|
335
|
+
weight: 2,
|
|
336
|
+
opacity: 0.8
|
|
337
|
+
});
|
|
338
|
+
layer._path.style.pointerEvents = this.isMultiPolygon ? 'stroke' : 'none';
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Get the outer ring coordinates
|
|
342
|
+
// For Polygon: coordinates[0] is the outer ring
|
|
343
|
+
// For MultiPolygon: coordinates[0][0] is the first polygon's outer ring
|
|
344
|
+
// Use current edited geometry if available
|
|
345
|
+
const ringGeometryToUse = this.currentEditedGeometry || this.value;
|
|
346
|
+
let outerRing = [];
|
|
347
|
+
|
|
348
|
+
if (this.isMultiPolygon) {
|
|
349
|
+
// MultiPolygon structure: [[[[lng, lat], ...]], [[[lng, lat], ...]]]
|
|
350
|
+
// Get the first polygon's outer ring
|
|
351
|
+
if (ringGeometryToUse.coordinates[0] && ringGeometryToUse.coordinates[0][0]) {
|
|
352
|
+
outerRing = ringGeometryToUse.coordinates[0][0];
|
|
353
|
+
}
|
|
354
|
+
} else {
|
|
355
|
+
// Polygon structure: [[[lng, lat], ...]]
|
|
356
|
+
outerRing = ringGeometryToUse.coordinates[0] || [];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (outerRing.length === 0) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Determine how many markers we should have (accounting for closed rings)
|
|
364
|
+
const isClosedRing = outerRing.length > 0 &&
|
|
365
|
+
outerRing[0][0] === outerRing[outerRing.length - 1][0] &&
|
|
366
|
+
outerRing[0][1] === outerRing[outerRing.length - 1][1];
|
|
367
|
+
const expectedMarkerCount = isClosedRing ? outerRing.length - 1 : outerRing.length;
|
|
368
|
+
|
|
369
|
+
// Remove existing markers if count doesn't match
|
|
370
|
+
if (this.draggableMarkers.length !== expectedMarkerCount && !this.isCreatingMarkers) {
|
|
371
|
+
this.isCreatingMarkers = true;
|
|
372
|
+
this.draggableMarkers.forEach(marker => {
|
|
373
|
+
if (marker) marker.remove();
|
|
374
|
+
});
|
|
375
|
+
this.draggableMarkers = [];
|
|
376
|
+
|
|
377
|
+
// Create draggable markers for each vertex
|
|
378
|
+
// Use setTimeout to ensure markers are added after polygon layer is fully rendered
|
|
379
|
+
this.$nextTick(() => {
|
|
380
|
+
// GeoJSON polygons typically have the first and last coordinate the same (closed ring)
|
|
381
|
+
// We'll create markers for all coordinates except the last one if it's a duplicate
|
|
382
|
+
// But we need to track the original index for updating the correct coordinate
|
|
383
|
+
const isClosedRing = outerRing.length > 0 &&
|
|
384
|
+
outerRing[0][0] === outerRing[outerRing.length - 1][0] &&
|
|
385
|
+
outerRing[0][1] === outerRing[outerRing.length - 1][1];
|
|
386
|
+
|
|
387
|
+
const coordsToProcess = isClosedRing
|
|
388
|
+
? outerRing.slice(0, -1) // Skip last coordinate if it's a duplicate of first
|
|
389
|
+
: outerRing;
|
|
390
|
+
|
|
391
|
+
coordsToProcess.forEach((coord, visibleIndex) => {
|
|
392
|
+
// visibleIndex is the index in the visible markers array
|
|
393
|
+
// actualIndex is the index in the coordinates array (same for non-closed rings)
|
|
394
|
+
const actualIndex = visibleIndex;
|
|
395
|
+
if (!Array.isArray(coord) || coord.length < 2) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const [lng, lat] = coord;
|
|
400
|
+
|
|
401
|
+
// Create a custom icon for the vertex marker
|
|
402
|
+
const icon = L.divIcon({
|
|
403
|
+
className: 'polygon-vertex-marker',
|
|
404
|
+
html: '<div style="width: 12px; height: 12px; background-color: #3388ff; border: 2px solid white; border-radius: 50%; cursor: move;"></div>',
|
|
405
|
+
iconSize: [12, 12],
|
|
406
|
+
iconAnchor: [6, 6]
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const marker = L.marker([lat, lng], {
|
|
410
|
+
draggable: true,
|
|
411
|
+
icon: icon,
|
|
412
|
+
zIndexOffset: 1000, // Ensure markers are above polygon layer
|
|
413
|
+
riseOnHover: true, // Bring marker to front on hover
|
|
414
|
+
bubblingMouseEvents: false // Prevent events from bubbling to polygon
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Add marker to map
|
|
418
|
+
marker.addTo(this.mapInstance);
|
|
419
|
+
|
|
420
|
+
// Ensure marker is in the marker pane (above overlay pane where polygon is)
|
|
421
|
+
// Note: markers don't have bringToFront(), we use zIndexOffset and DOM manipulation
|
|
422
|
+
if (marker._icon) {
|
|
423
|
+
marker._icon.style.pointerEvents = 'auto';
|
|
424
|
+
marker._icon.style.zIndex = (1001 + visibleIndex).toString(); // Unique z-index for each marker
|
|
425
|
+
// Move marker icon to top of marker pane
|
|
426
|
+
if (marker._icon.parentNode) {
|
|
427
|
+
marker._icon.parentNode.appendChild(marker._icon);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Add dragend event handler
|
|
432
|
+
// Store the actualIndex in closure for this marker
|
|
433
|
+
const markerActualIndex = actualIndex;
|
|
434
|
+
const markerIsFirstInClosedRing = isClosedRing && actualIndex === 0;
|
|
435
|
+
|
|
436
|
+
// Add right-click handler to show context menu
|
|
437
|
+
marker.on('contextmenu', (e) => {
|
|
438
|
+
e.originalEvent.preventDefault();
|
|
439
|
+
e.originalEvent.stopPropagation();
|
|
440
|
+
this.showContextMenu(e.originalEvent, markerActualIndex, marker);
|
|
441
|
+
return false;
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Also attach directly to icon element as the event might not bubble to marker
|
|
445
|
+
if (marker._icon) {
|
|
446
|
+
marker._icon.addEventListener('contextmenu', (e) => {
|
|
447
|
+
e.preventDefault();
|
|
448
|
+
e.stopPropagation();
|
|
449
|
+
this.showContextMenu(e, markerActualIndex, marker);
|
|
450
|
+
return false;
|
|
451
|
+
}, true); // Use capture phase
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
marker.on('dragend', () => {
|
|
455
|
+
const newLat = marker.getLatLng().lat;
|
|
456
|
+
const newLng = marker.getLatLng().lng;
|
|
457
|
+
|
|
458
|
+
// Use current edited geometry if available, otherwise use original value
|
|
459
|
+
const baseGeometry = this.currentEditedGeometry || this.value;
|
|
460
|
+
const newCoordinates = JSON.parse(JSON.stringify(baseGeometry.coordinates));
|
|
461
|
+
|
|
462
|
+
// Get the outer ring to check if it's closed
|
|
463
|
+
let outerRing = [];
|
|
464
|
+
if (this.isMultiPolygon) {
|
|
465
|
+
outerRing = newCoordinates[0][0] || [];
|
|
466
|
+
} else {
|
|
467
|
+
outerRing = newCoordinates[0] || [];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Check if this is a closed ring
|
|
471
|
+
const isClosedRingNow = outerRing.length > 0 &&
|
|
472
|
+
outerRing[0][0] === outerRing[outerRing.length - 1][0] &&
|
|
473
|
+
outerRing[0][1] === outerRing[outerRing.length - 1][1];
|
|
474
|
+
|
|
475
|
+
// Update the coordinate
|
|
476
|
+
if (this.isMultiPolygon) {
|
|
477
|
+
// MultiPolygon: coordinates[0][0][index] = [lng, lat]
|
|
478
|
+
newCoordinates[0][0][markerActualIndex] = [newLng, newLat];
|
|
479
|
+
// If closed ring and this is the first coordinate, also update the closing coordinate
|
|
480
|
+
if (isClosedRingNow && markerIsFirstInClosedRing) {
|
|
481
|
+
newCoordinates[0][0][outerRing.length - 1] = [newLng, newLat];
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
// Polygon: coordinates[0][index] = [lng, lat]
|
|
485
|
+
newCoordinates[0][markerActualIndex] = [newLng, newLat];
|
|
486
|
+
// If closed ring and this is the first coordinate, also update the closing coordinate
|
|
487
|
+
if (isClosedRingNow && markerIsFirstInClosedRing) {
|
|
488
|
+
newCoordinates[0][outerRing.length - 1] = [newLng, newLat];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Validate coordinate structure before creating geometry
|
|
493
|
+
try {
|
|
494
|
+
// Ensure coordinates are valid numbers
|
|
495
|
+
if (isNaN(newLng) || isNaN(newLat)) {
|
|
496
|
+
throw new Error(`Invalid coordinates: [${newLng}, ${newLat}]`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const newGeometry = {
|
|
500
|
+
type: baseGeometry.type, // Preserve Polygon or MultiPolygon type
|
|
501
|
+
coordinates: newCoordinates
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// Validate the geometry structure
|
|
505
|
+
if (!newGeometry.type || !Array.isArray(newGeometry.coordinates)) {
|
|
506
|
+
throw new Error('Invalid geometry structure');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Store the current edited geometry state
|
|
510
|
+
this.currentEditedGeometry = newGeometry;
|
|
511
|
+
this.hasUnsavedChanges = true;
|
|
512
|
+
|
|
513
|
+
// Update the polygon layer immediately for visual feedback
|
|
514
|
+
this.updatePolygonLayer(newGeometry);
|
|
515
|
+
|
|
516
|
+
// Notify parent of the change
|
|
517
|
+
if (this.onChange) {
|
|
518
|
+
this.onChange(newGeometry);
|
|
519
|
+
}
|
|
520
|
+
} catch (error) {
|
|
521
|
+
// Log errors to aid debugging while still preventing UI crashes
|
|
522
|
+
console.error('Error updating geometry on marker dragend:', error);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
this.draggableMarkers.push(marker);
|
|
527
|
+
});
|
|
528
|
+
// Reset the guard after all markers are created
|
|
529
|
+
this.isCreatingMarkers = false;
|
|
530
|
+
});
|
|
531
|
+
} else {
|
|
532
|
+
// Update existing marker positions if coordinates changed
|
|
533
|
+
// Get the correct outer ring based on geometry type
|
|
534
|
+
// Use current edited geometry if available
|
|
535
|
+
const updateGeometryToUse = this.currentEditedGeometry || this.value;
|
|
536
|
+
let currentOuterRing = [];
|
|
537
|
+
if (this.isMultiPolygon) {
|
|
538
|
+
if (updateGeometryToUse.coordinates[0] && updateGeometryToUse.coordinates[0][0]) {
|
|
539
|
+
currentOuterRing = updateGeometryToUse.coordinates[0][0];
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
currentOuterRing = updateGeometryToUse.coordinates[0] || [];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
currentOuterRing.forEach((coord, index) => {
|
|
546
|
+
const [lng, lat] = coord;
|
|
547
|
+
const marker = this.draggableMarkers[index];
|
|
548
|
+
if (marker) {
|
|
549
|
+
const currentLatLng = marker.getLatLng();
|
|
550
|
+
// Only update if coordinates actually changed (avoid interrupting drag)
|
|
551
|
+
if (Math.abs(currentLatLng.lat - lat) > 0.0001 || Math.abs(currentLatLng.lng - lng) > 0.0001) {
|
|
552
|
+
marker.setLatLng([lat, lng]);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Update polygon layer to reflect any coordinate changes
|
|
558
|
+
// Use current edited geometry if available, otherwise use value
|
|
559
|
+
const layerGeometryToUse = this.currentEditedGeometry || this.value;
|
|
560
|
+
this.updatePolygonLayer(layerGeometryToUse);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Remove point marker if it exists
|
|
564
|
+
if (this.draggableMarker) {
|
|
565
|
+
this.draggableMarkers = this.draggableMarkers.filter(marker => marker !== this.draggableMarker);
|
|
566
|
+
this.draggableMarker.remove();
|
|
567
|
+
this.draggableMarker = null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Fit bounds to polygon
|
|
571
|
+
const bounds = this.mapLayer.getBounds();
|
|
572
|
+
if (bounds.isValid()) {
|
|
573
|
+
const currentZoom = this.mapInstance.getZoom();
|
|
574
|
+
if (currentZoom < 10) {
|
|
575
|
+
this.mapInstance.fitBounds(bounds, { maxZoom: 16 });
|
|
576
|
+
} else {
|
|
577
|
+
// Just ensure polygon is visible, don't change zoom if already zoomed in
|
|
578
|
+
if (!bounds.contains(this.mapInstance.getBounds())) {
|
|
579
|
+
this.mapInstance.fitBounds(bounds, { maxZoom: 16 });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
// For other geometries or non-editable mode, use standard GeoJSON layer
|
|
585
|
+
if (this.draggableMarker) {
|
|
586
|
+
this.draggableMarker.remove();
|
|
587
|
+
this.draggableMarker = null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Clean up polygon markers
|
|
591
|
+
this.draggableMarkers.forEach(marker => {
|
|
592
|
+
if (marker) marker.remove();
|
|
593
|
+
});
|
|
594
|
+
this.draggableMarkers = [];
|
|
595
|
+
|
|
596
|
+
if (this.mapLayer) {
|
|
597
|
+
this.mapLayer.remove();
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const feature = {
|
|
601
|
+
type: 'Feature',
|
|
602
|
+
geometry: this.value,
|
|
603
|
+
properties: {}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
this.mapLayer = L.geoJSON(feature).addTo(this.mapInstance);
|
|
607
|
+
const bounds = this.mapLayer.getBounds();
|
|
608
|
+
if (bounds.isValid()) {
|
|
609
|
+
this.mapInstance.fitBounds(bounds, { maxZoom: 16 });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
updatePolygonLayer(geometry) {
|
|
614
|
+
if (!this.mapInstance || !this.mapLayer) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Remove old layer
|
|
619
|
+
this.mapLayer.remove();
|
|
620
|
+
|
|
621
|
+
// Create new layer with updated geometry
|
|
622
|
+
const feature = {
|
|
623
|
+
type: 'Feature',
|
|
624
|
+
geometry: geometry,
|
|
625
|
+
properties: {}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
this.mapLayer = L.geoJSON(feature, {
|
|
629
|
+
style: {
|
|
630
|
+
color: '#3388ff',
|
|
631
|
+
weight: 2,
|
|
632
|
+
opacity: 0.8,
|
|
633
|
+
fillOpacity: 0.2
|
|
634
|
+
},
|
|
635
|
+
interactive: this.isMultiPolygon // Only interactive for MultiPolygon (to allow edge clicks)
|
|
636
|
+
}).addTo(this.mapInstance);
|
|
637
|
+
|
|
638
|
+
// Add contextmenu handler to polygon edges to add vertices (only for MultiPolygon)
|
|
639
|
+
if (this.isMultiPolygon && this.mapLayer.eachLayer) {
|
|
640
|
+
this.mapLayer.eachLayer((layer) => {
|
|
641
|
+
// Remove any existing contextmenu handlers first
|
|
642
|
+
layer.off('contextmenu');
|
|
643
|
+
|
|
644
|
+
layer.on('contextmenu', (e) => {
|
|
645
|
+
e.originalEvent.preventDefault();
|
|
646
|
+
e.originalEvent.stopPropagation();
|
|
647
|
+
|
|
648
|
+
const clickPoint = e.latlng;
|
|
649
|
+
const clickContainerPoint = this.mapInstance.latLngToContainerPoint(clickPoint);
|
|
650
|
+
|
|
651
|
+
// Check if clicking near an existing marker
|
|
652
|
+
const isClickingOnMarker = this.draggableMarkers.some(marker => {
|
|
653
|
+
if (!marker || !marker._icon) return false;
|
|
654
|
+
const markerLatLng = marker.getLatLng();
|
|
655
|
+
const markerContainerPoint = this.mapInstance.latLngToContainerPoint(markerLatLng);
|
|
656
|
+
|
|
657
|
+
const dx = clickContainerPoint.x - markerContainerPoint.x;
|
|
658
|
+
const dy = clickContainerPoint.y - markerContainerPoint.y;
|
|
659
|
+
const pixelDistance = Math.sqrt(dx * dx + dy * dy);
|
|
660
|
+
|
|
661
|
+
return pixelDistance < 15;
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
if (!isClickingOnMarker) {
|
|
665
|
+
this.showAddVertexContextMenu(e.originalEvent, clickPoint);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Style polygon layer
|
|
670
|
+
if (layer.setStyle) {
|
|
671
|
+
layer.setStyle({
|
|
672
|
+
interactive: true,
|
|
673
|
+
stroke: true,
|
|
674
|
+
weight: 2,
|
|
675
|
+
opacity: 0.8
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (layer._path) {
|
|
680
|
+
layer._path.style.pointerEvents = 'stroke';
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
} else if (!this.isMultiPolygon && this.mapLayer.eachLayer) {
|
|
684
|
+
// For regular Polygon, ensure it's non-interactive
|
|
685
|
+
this.mapLayer.eachLayer((layer) => {
|
|
686
|
+
layer.off('contextmenu'); // Remove any contextmenu handlers
|
|
687
|
+
if (layer.setStyle) {
|
|
688
|
+
layer.setStyle({ interactive: false });
|
|
689
|
+
}
|
|
690
|
+
if (layer._path) {
|
|
691
|
+
layer._path.style.pointerEvents = 'none';
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Bring all markers to front after updating polygon
|
|
697
|
+
// Note: markers don't have bringToFront(), we manipulate DOM directly
|
|
698
|
+
this.draggableMarkers.forEach((marker, index) => {
|
|
699
|
+
if (marker && marker._icon && marker._icon.parentNode) {
|
|
700
|
+
marker._icon.style.zIndex = (1001 + index).toString();
|
|
701
|
+
marker._icon.parentNode.appendChild(marker._icon);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
},
|
|
705
|
+
showContextMenu(event, index, marker) {
|
|
706
|
+
// Hide any existing context menu
|
|
707
|
+
this.hideContextMenu();
|
|
708
|
+
|
|
709
|
+
// Store the marker for deletion
|
|
710
|
+
this.contextMenuMarker = { index, marker, type: 'vertex' };
|
|
711
|
+
|
|
712
|
+
// Create context menu if it doesn't exist
|
|
713
|
+
if (!this.contextMenu) {
|
|
714
|
+
this.contextMenu = document.createElement('div');
|
|
715
|
+
this.contextMenu.className = 'leaflet-context-menu';
|
|
716
|
+
|
|
717
|
+
// Append to map container so it's positioned relative to the map
|
|
718
|
+
if (this.mapInstance && this.mapInstance.getContainer()) {
|
|
719
|
+
this.mapInstance.getContainer().appendChild(this.contextMenu);
|
|
720
|
+
} else {
|
|
721
|
+
document.body.appendChild(this.contextMenu);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Clear existing menu items
|
|
726
|
+
this.contextMenu.innerHTML = '';
|
|
727
|
+
|
|
728
|
+
// Add Delete option for vertices
|
|
729
|
+
const deleteItem = document.createElement('div');
|
|
730
|
+
deleteItem.className = 'leaflet-context-menu-item delete';
|
|
731
|
+
deleteItem.textContent = 'Delete';
|
|
732
|
+
deleteItem.addEventListener('click', (e) => {
|
|
733
|
+
e.stopPropagation();
|
|
734
|
+
if (this.contextMenuMarker && this.contextMenuMarker.type === 'vertex') {
|
|
735
|
+
this.deleteVertex(this.contextMenuMarker.index, this.contextMenuMarker.marker);
|
|
736
|
+
}
|
|
737
|
+
this.hideContextMenu();
|
|
738
|
+
});
|
|
739
|
+
this.contextMenu.appendChild(deleteItem);
|
|
740
|
+
|
|
741
|
+
// Get map container position for relative positioning
|
|
742
|
+
const mapContainer = this.mapInstance ? this.mapInstance.getContainer() : null;
|
|
743
|
+
let left = event.clientX;
|
|
744
|
+
let top = event.clientY;
|
|
745
|
+
|
|
746
|
+
if (mapContainer) {
|
|
747
|
+
const rect = mapContainer.getBoundingClientRect();
|
|
748
|
+
left = event.clientX - rect.left;
|
|
749
|
+
top = event.clientY - rect.top;
|
|
750
|
+
this.contextMenu.style.position = 'absolute';
|
|
751
|
+
} else {
|
|
752
|
+
this.contextMenu.style.position = 'fixed';
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Position the context menu at the click location
|
|
756
|
+
this.contextMenu.style.left = left + 'px';
|
|
757
|
+
this.contextMenu.style.top = top + 'px';
|
|
758
|
+
this.contextMenu.style.display = 'block';
|
|
759
|
+
|
|
760
|
+
// Hide context menu when clicking elsewhere
|
|
761
|
+
const hideMenu = (e) => {
|
|
762
|
+
if (this.contextMenu && !this.contextMenu.contains(e.target)) {
|
|
763
|
+
this.hideContextMenu();
|
|
764
|
+
document.removeEventListener('click', hideMenu);
|
|
765
|
+
document.removeEventListener('contextmenu', hideMenu);
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
// Use setTimeout to avoid immediate hide from the current click
|
|
770
|
+
setTimeout(() => {
|
|
771
|
+
document.addEventListener('click', hideMenu);
|
|
772
|
+
document.addEventListener('contextmenu', hideMenu);
|
|
773
|
+
}, 10);
|
|
774
|
+
},
|
|
775
|
+
hideContextMenu() {
|
|
776
|
+
if (this.contextMenu) {
|
|
777
|
+
this.contextMenu.style.display = 'none';
|
|
778
|
+
}
|
|
779
|
+
this.contextMenuMarker = null;
|
|
780
|
+
},
|
|
781
|
+
showAddVertexContextMenu(event, latlng) {
|
|
782
|
+
// Hide any existing context menu
|
|
783
|
+
this.hideContextMenu();
|
|
784
|
+
|
|
785
|
+
// Store the location for adding vertex
|
|
786
|
+
this.contextMenuMarker = { latlng, type: 'edge' };
|
|
787
|
+
|
|
788
|
+
// Create context menu if it doesn't exist
|
|
789
|
+
if (!this.contextMenu) {
|
|
790
|
+
this.contextMenu = document.createElement('div');
|
|
791
|
+
this.contextMenu.className = 'leaflet-context-menu';
|
|
792
|
+
|
|
793
|
+
// Append to map container so it's positioned relative to the map
|
|
794
|
+
if (this.mapInstance && this.mapInstance.getContainer()) {
|
|
795
|
+
this.mapInstance.getContainer().appendChild(this.contextMenu);
|
|
796
|
+
} else {
|
|
797
|
+
document.body.appendChild(this.contextMenu);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Clear existing menu items
|
|
802
|
+
this.contextMenu.innerHTML = '';
|
|
803
|
+
|
|
804
|
+
// Add "Add Vertex" option
|
|
805
|
+
const addVertexItem = document.createElement('div');
|
|
806
|
+
addVertexItem.className = 'leaflet-context-menu-item';
|
|
807
|
+
addVertexItem.textContent = 'Add Vertex';
|
|
808
|
+
addVertexItem.addEventListener('click', (e) => {
|
|
809
|
+
e.stopPropagation();
|
|
810
|
+
if (this.contextMenuMarker && this.contextMenuMarker.type === 'edge') {
|
|
811
|
+
this.addVertexAtLocation(this.contextMenuMarker.latlng);
|
|
812
|
+
}
|
|
813
|
+
this.hideContextMenu();
|
|
814
|
+
});
|
|
815
|
+
this.contextMenu.appendChild(addVertexItem);
|
|
816
|
+
|
|
817
|
+
// Position the context menu
|
|
818
|
+
const mapContainer = this.mapInstance ? this.mapInstance.getContainer() : null;
|
|
819
|
+
let left = event.clientX;
|
|
820
|
+
let top = event.clientY;
|
|
821
|
+
|
|
822
|
+
if (mapContainer) {
|
|
823
|
+
const rect = mapContainer.getBoundingClientRect();
|
|
824
|
+
left = event.clientX - rect.left;
|
|
825
|
+
top = event.clientY - rect.top;
|
|
826
|
+
this.contextMenu.style.position = 'absolute';
|
|
827
|
+
} else {
|
|
828
|
+
this.contextMenu.style.position = 'fixed';
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
this.contextMenu.style.left = left + 'px';
|
|
832
|
+
this.contextMenu.style.top = top + 'px';
|
|
833
|
+
this.contextMenu.style.display = 'block';
|
|
834
|
+
|
|
835
|
+
// Hide menu when clicking elsewhere
|
|
836
|
+
const hideMenu = (e) => {
|
|
837
|
+
if (!this.contextMenu || !this.contextMenu.contains(e.target)) {
|
|
838
|
+
this.hideContextMenu();
|
|
839
|
+
document.removeEventListener('click', hideMenu);
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
setTimeout(() => {
|
|
843
|
+
document.addEventListener('click', hideMenu);
|
|
844
|
+
}, 0);
|
|
845
|
+
},
|
|
846
|
+
addVertexAtLocation(latlng) {
|
|
847
|
+
// Get current geometry
|
|
848
|
+
const baseGeometry = this.currentEditedGeometry || this.value;
|
|
849
|
+
const newCoordinates = JSON.parse(JSON.stringify(baseGeometry.coordinates));
|
|
850
|
+
|
|
851
|
+
// Get the outer ring
|
|
852
|
+
let outerRing = [];
|
|
853
|
+
if (this.isMultiPolygon) {
|
|
854
|
+
outerRing = newCoordinates[0][0] || [];
|
|
855
|
+
} else {
|
|
856
|
+
outerRing = newCoordinates[0] || [];
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (outerRing.length === 0) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Check if this is a closed ring
|
|
864
|
+
const isClosedRing = outerRing.length > 0 &&
|
|
865
|
+
outerRing[0][0] === outerRing[outerRing.length - 1][0] &&
|
|
866
|
+
outerRing[0][1] === outerRing[outerRing.length - 1][1];
|
|
867
|
+
|
|
868
|
+
// Convert latlng to [lng, lat] format
|
|
869
|
+
const newCoord = [latlng.lng, latlng.lat];
|
|
870
|
+
|
|
871
|
+
// Find the closest edge segment and insert the vertex
|
|
872
|
+
let closestEdgeIndex = 0;
|
|
873
|
+
let minDistance = Infinity;
|
|
874
|
+
|
|
875
|
+
// Check each edge segment
|
|
876
|
+
for (let i = 0; i < outerRing.length - 1; i++) {
|
|
877
|
+
const p1 = outerRing[i];
|
|
878
|
+
const p2 = outerRing[i + 1];
|
|
879
|
+
|
|
880
|
+
// Calculate distance from click point to edge segment
|
|
881
|
+
const distance = this.distanceToLineSegment(
|
|
882
|
+
[latlng.lng, latlng.lat],
|
|
883
|
+
[p1[0], p1[1]],
|
|
884
|
+
[p2[0], p2[1]]
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
if (distance < minDistance) {
|
|
888
|
+
minDistance = distance;
|
|
889
|
+
closestEdgeIndex = i + 1; // Insert after point i
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// For closed rings, also check the edge from last to first
|
|
894
|
+
if (isClosedRing) {
|
|
895
|
+
const p1 = outerRing[outerRing.length - 1];
|
|
896
|
+
const p2 = outerRing[0];
|
|
897
|
+
const distance = this.distanceToLineSegment(
|
|
898
|
+
[latlng.lng, latlng.lat],
|
|
899
|
+
[p1[0], p1[1]],
|
|
900
|
+
[p2[0], p2[1]]
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
if (distance < minDistance) {
|
|
904
|
+
minDistance = distance;
|
|
905
|
+
closestEdgeIndex = outerRing.length; // Insert at end (before closing coordinate)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Insert the new coordinate
|
|
910
|
+
if (this.isMultiPolygon) {
|
|
911
|
+
newCoordinates[0][0].splice(closestEdgeIndex, 0, newCoord);
|
|
912
|
+
// If it was a closed ring, update the closing coordinate
|
|
913
|
+
if (isClosedRing) {
|
|
914
|
+
newCoordinates[0][0][newCoordinates[0][0].length - 1] = newCoordinates[0][0][0];
|
|
915
|
+
}
|
|
916
|
+
} else {
|
|
917
|
+
newCoordinates[0].splice(closestEdgeIndex, 0, newCoord);
|
|
918
|
+
// If it was a closed ring, update the closing coordinate
|
|
919
|
+
if (isClosedRing) {
|
|
920
|
+
newCoordinates[0][newCoordinates[0].length - 1] = newCoordinates[0][0];
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const newGeometry = {
|
|
925
|
+
type: baseGeometry.type,
|
|
926
|
+
coordinates: newCoordinates
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// Store the current edited geometry state
|
|
930
|
+
this.currentEditedGeometry = newGeometry;
|
|
931
|
+
this.hasUnsavedChanges = true;
|
|
932
|
+
|
|
933
|
+
// Update the polygon layer and recreate markers
|
|
934
|
+
this.updatePolygonLayer(newGeometry);
|
|
935
|
+
this.$nextTick(() => {
|
|
936
|
+
this.updateMapLayer();
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// Notify parent of the change
|
|
940
|
+
if (this.onChange) {
|
|
941
|
+
this.onChange(newGeometry);
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
distanceToLineSegment(point, lineStart, lineEnd) {
|
|
945
|
+
// Calculate distance from point to line segment
|
|
946
|
+
// point, lineStart, lineEnd are [lng, lat] arrays
|
|
947
|
+
const A = point[0] - lineStart[0];
|
|
948
|
+
const B = point[1] - lineStart[1];
|
|
949
|
+
const C = lineEnd[0] - lineStart[0];
|
|
950
|
+
const D = lineEnd[1] - lineStart[1];
|
|
951
|
+
|
|
952
|
+
const dot = A * C + B * D;
|
|
953
|
+
const lenSq = C * C + D * D;
|
|
954
|
+
let param = -1;
|
|
955
|
+
|
|
956
|
+
if (lenSq !== 0) {
|
|
957
|
+
param = dot / lenSq;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
let xx, yy;
|
|
961
|
+
|
|
962
|
+
if (param < 0) {
|
|
963
|
+
xx = lineStart[0];
|
|
964
|
+
yy = lineStart[1];
|
|
965
|
+
} else if (param > 1) {
|
|
966
|
+
xx = lineEnd[0];
|
|
967
|
+
yy = lineEnd[1];
|
|
968
|
+
} else {
|
|
969
|
+
xx = lineStart[0] + param * C;
|
|
970
|
+
yy = lineStart[1] + param * D;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const dx = point[0] - xx;
|
|
974
|
+
const dy = point[1] - yy;
|
|
975
|
+
|
|
976
|
+
// Use simple Euclidean distance (approximation for small areas)
|
|
977
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
978
|
+
},
|
|
979
|
+
deleteVertex(index, marker) {
|
|
980
|
+
// Get current geometry
|
|
981
|
+
const baseGeometry = this.currentEditedGeometry || this.value;
|
|
982
|
+
const newCoordinates = JSON.parse(JSON.stringify(baseGeometry.coordinates));
|
|
983
|
+
|
|
984
|
+
// Get the outer ring
|
|
985
|
+
let outerRing = [];
|
|
986
|
+
if (this.isMultiPolygon) {
|
|
987
|
+
outerRing = newCoordinates[0][0] || [];
|
|
988
|
+
} else {
|
|
989
|
+
outerRing = newCoordinates[0] || [];
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Check if this is a closed ring
|
|
993
|
+
const isClosedRing = outerRing.length > 0 &&
|
|
994
|
+
outerRing[0][0] === outerRing[outerRing.length - 1][0] &&
|
|
995
|
+
outerRing[0][1] === outerRing[outerRing.length - 1][1];
|
|
996
|
+
|
|
997
|
+
// Minimum vertices for a valid polygon (3 for triangle, but GeoJSON requires at least 4 for closed ring)
|
|
998
|
+
const minVertices = isClosedRing ? 4 : 3;
|
|
999
|
+
|
|
1000
|
+
// Don't allow deletion if we'd have too few vertices
|
|
1001
|
+
const currentVertexCount = isClosedRing ? outerRing.length - 1 : outerRing.length;
|
|
1002
|
+
if (currentVertexCount < minVertices) {
|
|
1003
|
+
const message = isClosedRing
|
|
1004
|
+
? `Cannot delete vertex. A polygon requires at least ${minVertices} vertices (including the closing vertex).`
|
|
1005
|
+
: `Cannot delete vertex. A polygon requires at least ${minVertices} vertices.`;
|
|
1006
|
+
this.$toast.error(message, {
|
|
1007
|
+
timeout: 5000
|
|
1008
|
+
});
|
|
1009
|
+
this.hideContextMenu();
|
|
1010
|
+
return; // Can't delete - would make invalid polygon
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Remove the coordinate
|
|
1014
|
+
if (this.isMultiPolygon) {
|
|
1015
|
+
newCoordinates[0][0].splice(index, 1);
|
|
1016
|
+
// If it was a closed ring and we removed a coordinate, update the closing coordinate
|
|
1017
|
+
if (isClosedRing && index === 0) {
|
|
1018
|
+
// If we deleted the first coordinate, the new first becomes the closing coordinate
|
|
1019
|
+
newCoordinates[0][0][newCoordinates[0][0].length - 1] = newCoordinates[0][0][0];
|
|
1020
|
+
} else if (isClosedRing && index === outerRing.length - 1) {
|
|
1021
|
+
// If we deleted the closing coordinate, update it to match the first
|
|
1022
|
+
newCoordinates[0][0][newCoordinates[0][0].length - 1] = newCoordinates[0][0][0];
|
|
1023
|
+
}
|
|
1024
|
+
} else {
|
|
1025
|
+
newCoordinates[0].splice(index, 1);
|
|
1026
|
+
// If it was a closed ring and we removed a coordinate, update the closing coordinate
|
|
1027
|
+
if (isClosedRing && index === 0) {
|
|
1028
|
+
// If we deleted the first coordinate, the new first becomes the closing coordinate
|
|
1029
|
+
newCoordinates[0][newCoordinates[0].length - 1] = newCoordinates[0][0];
|
|
1030
|
+
} else if (isClosedRing && index === outerRing.length - 1) {
|
|
1031
|
+
// If we deleted the closing coordinate, update it to match the first
|
|
1032
|
+
newCoordinates[0][newCoordinates[0].length - 1] = newCoordinates[0][0];
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const newGeometry = {
|
|
1037
|
+
type: baseGeometry.type,
|
|
1038
|
+
coordinates: newCoordinates
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
// Store the current edited geometry state
|
|
1042
|
+
this.currentEditedGeometry = newGeometry;
|
|
1043
|
+
this.hasUnsavedChanges = true;
|
|
1044
|
+
|
|
1045
|
+
// Remove all markers and force recreation (needed because marker closures capture indices)
|
|
1046
|
+
this.draggableMarkers.forEach(m => {
|
|
1047
|
+
if (m) m.remove();
|
|
1048
|
+
});
|
|
1049
|
+
this.draggableMarkers = [];
|
|
1050
|
+
|
|
1051
|
+
// Update the polygon layer and recreate markers
|
|
1052
|
+
this.updatePolygonLayer(newGeometry);
|
|
1053
|
+
this.$nextTick(() => {
|
|
1054
|
+
this.updateMapLayer();
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
// Notify parent of the change
|
|
1058
|
+
if (this.onChange) {
|
|
1059
|
+
this.onChange(newGeometry);
|
|
1060
|
+
}
|
|
1061
|
+
},
|
|
1062
|
+
undoDelete() {
|
|
1063
|
+
// Restore the original geometry (undo all changes)
|
|
1064
|
+
if (!this.originalGeometry) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const restoredGeometry = JSON.parse(JSON.stringify(this.originalGeometry));
|
|
1069
|
+
|
|
1070
|
+
// Clear all edited state
|
|
1071
|
+
this.currentEditedGeometry = null;
|
|
1072
|
+
this.hasUnsavedChanges = false;
|
|
1073
|
+
|
|
1074
|
+
// Update the map to show the original geometry
|
|
1075
|
+
if (this.isGeoJsonPoint) {
|
|
1076
|
+
this.$nextTick(() => {
|
|
1077
|
+
this.updateMapLayer();
|
|
1078
|
+
});
|
|
1079
|
+
} else if (this.isGeoJsonPolygon) {
|
|
1080
|
+
this.updatePolygonLayer(restoredGeometry);
|
|
1081
|
+
this.$nextTick(() => {
|
|
1082
|
+
this.updateMapLayer();
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Notify parent of the change (restore to original)
|
|
1087
|
+
if (this.onChange) {
|
|
1088
|
+
this.onChange(restoredGeometry);
|
|
1089
|
+
}
|
|
26
1090
|
}
|
|
27
1091
|
}
|
|
28
|
-
});
|
|
1092
|
+
});
|