@kispace-io/gs-lib 1.1.8 → 1.2.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.
Files changed (61) hide show
  1. package/dist/base-map-builder.d.ts.map +1 -1
  2. package/dist/gs-model.d.ts +6 -0
  3. package/dist/gs-model.d.ts.map +1 -1
  4. package/dist/index.d.ts +3 -5
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +890 -288
  7. package/dist/index.js.map +1 -1
  8. package/dist/map-renderer.d.ts +94 -0
  9. package/dist/map-renderer.d.ts.map +1 -0
  10. package/dist/ml/gs-gs2ml.d.ts +96 -0
  11. package/dist/ml/gs-gs2ml.d.ts.map +1 -0
  12. package/dist/ml/gs-ml-adapters.d.ts +41 -0
  13. package/dist/ml/gs-ml-adapters.d.ts.map +1 -0
  14. package/dist/ml/gs-ml-lib.d.ts +17 -0
  15. package/dist/ml/gs-ml-lib.d.ts.map +1 -0
  16. package/dist/ml/gs-ml2gs.d.ts +10 -0
  17. package/dist/ml/gs-ml2gs.d.ts.map +1 -0
  18. package/dist/ml/gs-mlns.d.ts +10 -0
  19. package/dist/ml/gs-mlns.d.ts.map +1 -0
  20. package/dist/ml/index.d.ts +9 -0
  21. package/dist/ml/index.d.ts.map +1 -0
  22. package/dist/ml/maplibre-map-renderer.d.ts +66 -0
  23. package/dist/ml/maplibre-map-renderer.d.ts.map +1 -0
  24. package/dist/{gs-gs2ol.d.ts → ol/gs-gs2ol.d.ts} +2 -2
  25. package/dist/ol/gs-gs2ol.d.ts.map +1 -0
  26. package/dist/ol/gs-ol-adapters.d.ts.map +1 -0
  27. package/dist/{gs-lib.d.ts → ol/gs-ol-lib.d.ts} +4 -4
  28. package/dist/ol/gs-ol-lib.d.ts.map +1 -0
  29. package/dist/{gs-ol2gs.d.ts → ol/gs-ol2gs.d.ts} +1 -1
  30. package/dist/ol/gs-ol2gs.d.ts.map +1 -0
  31. package/dist/ol/gs-olns.d.ts.map +1 -0
  32. package/dist/ol/index.d.ts +9 -0
  33. package/dist/ol/index.d.ts.map +1 -0
  34. package/dist/ol/openlayers-map-renderer.d.ts +68 -0
  35. package/dist/ol/openlayers-map-renderer.d.ts.map +1 -0
  36. package/package.json +6 -2
  37. package/src/base-map-builder.ts +8 -9
  38. package/src/gs-model.ts +7 -1
  39. package/src/index.ts +12 -7
  40. package/src/map-renderer.ts +115 -0
  41. package/src/ml/gs-gs2ml.ts +717 -0
  42. package/src/ml/gs-ml-adapters.ts +134 -0
  43. package/src/ml/gs-ml-lib.ts +124 -0
  44. package/src/ml/gs-ml2gs.ts +66 -0
  45. package/src/ml/gs-mlns.ts +50 -0
  46. package/src/ml/index.ts +41 -0
  47. package/src/ml/maplibre-map-renderer.ts +428 -0
  48. package/src/{gs-gs2ol.ts → ol/gs-gs2ol.ts} +10 -4
  49. package/src/{gs-lib.ts → ol/gs-ol-lib.ts} +7 -6
  50. package/src/{gs-ol2gs.ts → ol/gs-ol2gs.ts} +1 -1
  51. package/src/ol/index.ts +21 -0
  52. package/src/ol/openlayers-map-renderer.ts +719 -0
  53. package/dist/gs-gs2ol.d.ts.map +0 -1
  54. package/dist/gs-lib.d.ts.map +0 -1
  55. package/dist/gs-ol-adapters.d.ts.map +0 -1
  56. package/dist/gs-ol2gs.d.ts.map +0 -1
  57. package/dist/gs-olns.d.ts.map +0 -1
  58. /package/dist/{gs-ol-adapters.d.ts → ol/gs-ol-adapters.d.ts} +0 -0
  59. /package/dist/{gs-olns.d.ts → ol/gs-olns.d.ts} +0 -0
  60. /package/src/{gs-ol-adapters.ts → ol/gs-ol-adapters.ts} +0 -0
  61. /package/src/{gs-olns.ts → ol/gs-olns.ts} +0 -0
@@ -0,0 +1,719 @@
1
+ import { MapOperations, MapRenderer, MapSyncEvent, ScreenshotResult } from "../map-renderer";
2
+ import {
3
+ GsMap,
4
+ GsSourceType,
5
+ KEY_NAME,
6
+ KEY_STATE,
7
+ KEY_UUID,
8
+ GsFeature,
9
+ GsGeometry,
10
+ ensureUuid,
11
+ getStyleForFeature
12
+ } from "../gs-model";
13
+ import { toOlLayer, cleanupEventSubscriptions, toOlStyle } from "./gs-gs2ol";
14
+ import { toGsFeature } from "./gs-ol2gs";
15
+ import { olLib } from "./gs-ol-lib";
16
+ import { v4 as uuidv4 } from 'uuid';
17
+ import {
18
+ Map as OlMap,
19
+ Feature,
20
+ style,
21
+ sphere,
22
+ geom,
23
+ layer as layerNS,
24
+ source as sourceNS,
25
+ interaction as interactionNS,
26
+ FeatureLike,
27
+ eventsCondition,
28
+ BaseLayer
29
+ } from "./gs-olns";
30
+
31
+ /**
32
+ * Lightweight helper to extract minimal GsFeature data for style evaluation
33
+ * Avoids the overhead of full toGsFeature() conversion
34
+ */
35
+ function getFeatureStyleData(feature: Feature): GsFeature {
36
+ const geometry = feature.getGeometry() as geom.Geometry;
37
+ return ensureUuid({
38
+ geometry: ensureUuid({
39
+ type: geometry.getType(),
40
+ coordinates: [] // Not needed for style rules
41
+ } as GsGeometry),
42
+ state: feature.get(KEY_STATE)
43
+ } as GsFeature);
44
+ }
45
+
46
+ /**
47
+ * OpenLayers map renderer that manages OpenLayers maps
48
+ * Provides complete isolation between the host app and the rendering engine
49
+ * User modules are handled by the toOlMap() function
50
+ */
51
+ export class OpenLayersMapRenderer implements MapRenderer {
52
+ public olMap?: OlMap; // Made public for backward compatibility
53
+ gsMap: GsMap;
54
+ private env?: any;
55
+ private onDirtyCallback?: () => void;
56
+ private onSyncCallback?: (event: MapSyncEvent) => void;
57
+ private isDestroyed: boolean = false;
58
+ private operations?: OpenLayersMapOperations;
59
+ private styleCache: Map<string, style.Style> = new Map();
60
+
61
+ constructor(gsMap: GsMap, env?: any) {
62
+ this.gsMap = gsMap;
63
+ this.env = env;
64
+ }
65
+
66
+ async reattached(): Promise<void> {
67
+ // OpenLayers doesn't need special handling for reattachment
68
+ // The map stays attached to its container element
69
+ }
70
+
71
+ async render(container: string | HTMLElement): Promise<void> {
72
+ try {
73
+ // Use the runtime library to render the map
74
+ this.olMap = await olLib({
75
+ containerSelector: container,
76
+ gsMap: this.gsMap,
77
+ env: this.env,
78
+ mapOptions: {
79
+ controls: { zoom: false, attribution: false }
80
+ }
81
+ });
82
+
83
+ // Create operations after map is available
84
+ this.operations = new OpenLayersMapOperations(this.olMap, this);
85
+
86
+ // Apply styling to all vector layers
87
+ this.applyStylesToLayers();
88
+
89
+ // Set up event listeners after the map is rendered
90
+ this.olMap.once('rendercomplete', () => {
91
+ this.setupEventListeners();
92
+ });
93
+
94
+ } catch (error) {
95
+ console.error('Failed to render map:', error);
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ private applyStylesToLayers(): void {
101
+ if (!this.olMap) return;
102
+
103
+ const layers = this.olMap.getLayers().getArray();
104
+ layers.forEach(layer => {
105
+ if (layer instanceof layerNS.Vector) {
106
+ this.applyStyleToVectorLayer(layer);
107
+ }
108
+ });
109
+ }
110
+
111
+ private applyStyleToVectorLayer(layer: layerNS.Vector): void {
112
+ const layerName = layer.get(KEY_NAME);
113
+
114
+ // Create a style function that applies rules to each feature
115
+ const styleFunction = (feature: FeatureLike) => {
116
+ // Only process actual Feature objects (not RenderFeature)
117
+ if (!(feature instanceof Feature)) {
118
+ return undefined;
119
+ }
120
+
121
+ // Extract minimal data needed for style evaluation (lightweight, no coordinate conversion)
122
+ const featureStyleData = getFeatureStyleData(feature);
123
+ const styleRules = this.gsMap.styleRules;
124
+ const stylesMap = this.gsMap.styles;
125
+
126
+ if (styleRules && stylesMap) {
127
+ const gsStyle = getStyleForFeature(featureStyleData, styleRules, stylesMap, layerName);
128
+ if (gsStyle && gsStyle.id) {
129
+ // Check cache first
130
+ let olStyle = this.styleCache.get(gsStyle.id);
131
+ if (!olStyle) {
132
+ // Convert and cache
133
+ olStyle = toOlStyle(gsStyle);
134
+ this.styleCache.set(gsStyle.id, olStyle);
135
+ }
136
+ return olStyle;
137
+ } else if (gsStyle) {
138
+ // Style without ID - can't cache, convert directly
139
+ return toOlStyle(gsStyle);
140
+ }
141
+ }
142
+
143
+ // Return undefined to use default OpenLayers styling
144
+ return undefined;
145
+ };
146
+
147
+ layer.setStyle(styleFunction);
148
+ }
149
+
150
+ private clearStyleCache(): void {
151
+ this.styleCache.clear();
152
+ }
153
+
154
+ async modelToUI(updatedGsMap?: GsMap): Promise<void> {
155
+ if (!this.olMap) {
156
+ throw new Error('Map not initialized');
157
+ }
158
+
159
+ // Update the gsMap if provided
160
+ if (updatedGsMap) {
161
+ this.gsMap = updatedGsMap;
162
+ }
163
+
164
+ // Clear style cache when model changes (styles/rules may have changed)
165
+ this.clearStyleCache();
166
+
167
+ // Get the container before destroying the map
168
+ const target = this.olMap.getTarget();
169
+ if (!target || typeof target === 'string') {
170
+ throw new Error('Map container not found or invalid');
171
+ }
172
+
173
+ this.destroy();
174
+
175
+ // Clear the DOM container
176
+ target.innerHTML = '';
177
+
178
+ this.isDestroyed = false;
179
+ await this.render(target);
180
+ }
181
+
182
+
183
+ getOperations(): MapOperations {
184
+ if (!this.operations) {
185
+ throw new Error("Operations not available - map not rendered yet");
186
+ }
187
+ return this.operations;
188
+ }
189
+
190
+ async getViewExtent(): Promise<number[]> {
191
+ console.debug("Getting view extent");
192
+ if (!this.olMap) {
193
+ throw new Error("Map not available for extent calculation");
194
+ }
195
+
196
+ const view = this.olMap.getView();
197
+ const extent = view.calculateExtent();
198
+ console.debug(`View extent: ${extent}`);
199
+ return extent;
200
+ }
201
+
202
+ async captureScreenshot(): Promise<ScreenshotResult> {
203
+ if (!this.olMap) {
204
+ return { success: false, error: 'Map not available' };
205
+ }
206
+
207
+ const olMap = this.olMap;
208
+
209
+ // Wait for the map to finish rendering
210
+ await new Promise<void>((resolve) => {
211
+ olMap.renderSync();
212
+ olMap.once('rendercomplete', () => resolve());
213
+ // Fallback timeout in case rendercomplete doesn't fire
214
+ setTimeout(() => resolve(), 2000);
215
+ });
216
+
217
+ const size = olMap.getSize();
218
+ const width = size ? size[0] : olMap.getViewport().clientWidth;
219
+ const height = size ? size[1] : olMap.getViewport().clientHeight;
220
+
221
+ try {
222
+ const canvas = olMap.getViewport().querySelector('canvas');
223
+ if (!canvas) {
224
+ return { success: false, error: 'Map canvas not found' };
225
+ }
226
+
227
+ const dataUrl = canvas.toDataURL('image/png');
228
+ return { success: true, dataUrl, width, height };
229
+ } catch (error: any) {
230
+ return { success: false, error: `Failed to capture canvas: ${error.message}` };
231
+ }
232
+ }
233
+
234
+ setOnDirty(callback: () => void): void {
235
+ this.onDirtyCallback = callback;
236
+ }
237
+
238
+ setOnSync(callback: (event: MapSyncEvent) => void): void {
239
+ this.onSyncCallback = callback;
240
+ }
241
+
242
+ triggerDirty(): void {
243
+ if (this.isDestroyed || !this.onDirtyCallback) return;
244
+ this.onDirtyCallback();
245
+ }
246
+
247
+ triggerSync(event: MapSyncEvent): void {
248
+ if (this.isDestroyed || !this.onSyncCallback) return;
249
+ this.onSyncCallback(event);
250
+ }
251
+
252
+ private syncViewToModel(): void {
253
+ if (!this.olMap) return;
254
+
255
+ const view = this.olMap.getView();
256
+ const center = view.getCenter();
257
+ const zoom = view.getZoom();
258
+ const rotation = view.getRotation();
259
+
260
+ // Notify host with specific view change event
261
+ if (center && zoom !== undefined) {
262
+ this.triggerSync({
263
+ type: 'viewChanged',
264
+ view: { center: center as [number, number], zoom, rotation }
265
+ });
266
+ }
267
+ }
268
+
269
+ public syncLayerFeaturesToModel(layerUuid: string): void {
270
+ if (!this.olMap) return;
271
+
272
+ const layers = this.olMap.getLayers();
273
+ let olLayer: BaseLayer | undefined;
274
+ for (let i = 0; i < layers.getLength(); i++) {
275
+ const layer = layers.item(i);
276
+ if (layer.get(KEY_UUID) === layerUuid) {
277
+ olLayer = layer;
278
+ break;
279
+ }
280
+ }
281
+
282
+ if (!olLayer || !(olLayer instanceof layerNS.Vector)) return;
283
+
284
+ const source = olLayer.getSource();
285
+ if (!source) return;
286
+
287
+ // Convert OpenLayers features back to GsFeatures
288
+ const olFeatures = source.getFeatures();
289
+ const gsFeatures = olFeatures.map((olFeature: Feature) => toGsFeature(olFeature));
290
+
291
+ // Notify host with features change event
292
+ this.triggerSync({
293
+ type: 'featuresChanged',
294
+ layerUuid,
295
+ features: gsFeatures
296
+ });
297
+ }
298
+
299
+ private setupEventListeners(): void {
300
+ if (!this.olMap) return;
301
+
302
+ // Sync view changes back to domain model before triggering dirty
303
+ this.olMap.getView().on('change:center', () => {
304
+ this.syncViewToModel();
305
+ this.triggerDirty();
306
+ });
307
+ this.olMap.getView().on('change:resolution', () => {
308
+ this.syncViewToModel();
309
+ this.triggerDirty();
310
+ });
311
+ this.olMap.getView().on('change:rotation', () => {
312
+ this.syncViewToModel();
313
+ this.triggerDirty();
314
+ });
315
+
316
+ this.olMap.getLayers().on('add', () => this.triggerDirty());
317
+ this.olMap.getLayers().on('remove', () => this.triggerDirty());
318
+
319
+ this.olMap.getControls().on('add', () => this.triggerDirty());
320
+ this.olMap.getControls().on('remove', () => this.triggerDirty());
321
+
322
+ this.olMap.getOverlays().on('add', () => this.triggerDirty());
323
+ this.olMap.getOverlays().on('remove', () => this.triggerDirty());
324
+ }
325
+
326
+
327
+ destroy(): void {
328
+ this.isDestroyed = true;
329
+ this.clearStyleCache();
330
+ // Cleanup operations (removes keyboard listeners) before destroying map
331
+ if (this.operations) {
332
+ const ops = this.operations as any;
333
+ if (ops.cleanup) {
334
+ ops.cleanup();
335
+ }
336
+ }
337
+ // Cleanup event subscriptions before disposing map
338
+ if (this.olMap) {
339
+ cleanupEventSubscriptions(this.olMap);
340
+ }
341
+ this.olMap?.dispose();
342
+ this.olMap = undefined;
343
+ }
344
+
345
+ }
346
+
347
+ /**
348
+ * OpenLayers-specific map operations implementation
349
+ */
350
+ export class OpenLayersMapOperations implements MapOperations {
351
+ private drawInteraction?: interactionNS.Draw;
352
+ private selectInteraction?: interactionNS.Select;
353
+ private activeDrawingLayerUuid?: string;
354
+ private keyDownListener?: (event: KeyboardEvent) => void;
355
+
356
+ constructor(
357
+ private olMap: OlMap,
358
+ private renderer?: OpenLayersMapRenderer
359
+ ) {
360
+ if (!olMap) {
361
+ throw new Error("OpenLayers map is required for operations");
362
+ }
363
+
364
+ // Setup ESC key handler
365
+ this.keyDownListener = (event: KeyboardEvent) => {
366
+ if (event.key === 'Escape') {
367
+ if (this.drawInteraction) {
368
+ this.disableDrawing();
369
+ this.renderer?.triggerSync({ type: 'drawingDisabled' } as any);
370
+ }
371
+ if (this.selectInteraction) {
372
+ this.disableSelection();
373
+ }
374
+ }
375
+ };
376
+
377
+ const target = this.olMap.getTargetElement();
378
+ if (target && target instanceof HTMLElement) {
379
+ target.setAttribute('tabindex', '-1');
380
+ target.addEventListener('keydown', this.keyDownListener);
381
+ }
382
+ }
383
+
384
+ async setZoom(zoom: number): Promise<void> {
385
+ this.olMap.getView().setZoom(zoom);
386
+ }
387
+
388
+ async setCenter(center: [number, number]): Promise<void> {
389
+ this.olMap.getView().setCenter(center);
390
+ }
391
+
392
+ async switchColorMode(mode?: 'dark' | 'light'): Promise<void> {
393
+ const olMap = this.olMap;
394
+ let darkMode: boolean = olMap.get("darkmode") ?? false;
395
+
396
+ if (mode === 'dark') {
397
+ darkMode = true;
398
+ } else if (mode === 'light') {
399
+ darkMode = false;
400
+ } else {
401
+ darkMode = !darkMode;
402
+ }
403
+
404
+ olMap.set("darkmode", darkMode);
405
+
406
+ const canvasElements = document.querySelectorAll('canvas');
407
+ canvasElements.forEach(canvas => {
408
+ canvas.style.filter = darkMode ? "invert(100%)" : "";
409
+ });
410
+
411
+ olMap.render();
412
+ }
413
+
414
+ async addLayer(layer: any, isBasemap?: boolean): Promise<void> {
415
+ const olLayer = toOlLayer(layer);
416
+ if (isBasemap) {
417
+ this.olMap.getLayers().insertAt(0, olLayer);
418
+ } else {
419
+ this.olMap.getLayers().push(olLayer);
420
+ }
421
+ }
422
+
423
+ async deleteLayer(uuid: string): Promise<void> {
424
+ const layers = this.olMap.getLayers();
425
+ for (let i = 0; i < layers.getLength(); i++) {
426
+ const layer = layers.item(i);
427
+ if (layer.get(KEY_UUID) === uuid) {
428
+ layers.removeAt(i);
429
+ return;
430
+ }
431
+ }
432
+ }
433
+
434
+ async renameLayer(uuid: string, newName: string): Promise<void> {
435
+ const layers = this.olMap.getLayers();
436
+ for (let i = 0; i < layers.getLength(); i++) {
437
+ const layer = layers.item(i);
438
+ if (layer.get(KEY_UUID) === uuid) {
439
+ layer.set(KEY_NAME, newName);
440
+ return;
441
+ }
442
+ }
443
+ }
444
+
445
+ async moveLayer(uuid: string, targetUuid?: string): Promise<void> {
446
+ const layers = this.olMap.getLayers();
447
+ let fromIndex = -1;
448
+ let toIndex = -1;
449
+
450
+ for (let i = 0; i < layers.getLength(); i++) {
451
+ const layer = layers.item(i);
452
+ if (layer.get(KEY_UUID) === uuid) {
453
+ fromIndex = i;
454
+ }
455
+ if (targetUuid && layer.get(KEY_UUID) === targetUuid) {
456
+ toIndex = i;
457
+ }
458
+ }
459
+
460
+ if (fromIndex < 0) return;
461
+
462
+ if (targetUuid) {
463
+ if (toIndex < 0 || fromIndex === toIndex) return;
464
+ } else {
465
+ toIndex = fromIndex > 0 ? fromIndex - 1 : fromIndex + 1;
466
+ }
467
+
468
+ if (toIndex >= 0 && toIndex < layers.getLength() && fromIndex !== toIndex) {
469
+ const layer = layers.item(fromIndex);
470
+ layers.removeAt(fromIndex);
471
+ layers.insertAt(toIndex, layer);
472
+ }
473
+ }
474
+
475
+ async setLayerVisible(uuid: string, visible: boolean): Promise<void> {
476
+ const layers = this.olMap.getLayers();
477
+ for (let i = 0; i < layers.getLength(); i++) {
478
+ const layer = layers.item(i);
479
+ if (layer.get(KEY_UUID) === uuid) {
480
+ layer.setVisible(visible);
481
+ return;
482
+ }
483
+ }
484
+ }
485
+
486
+ async addControlFromModule(_src: string): Promise<void> {}
487
+ async removeControl(_uuid: string): Promise<void> {}
488
+ async addOverlayFromModule(_src: string, _position?: string): Promise<void> {}
489
+ async removeOverlay(_uuid: string): Promise<void> {}
490
+
491
+ private setCursor(cursor: string): void {
492
+ const viewport = this.olMap.getViewport();
493
+ if (viewport) {
494
+ (viewport as HTMLElement).style.cursor = cursor;
495
+ }
496
+ }
497
+
498
+ async enableDrawing(geometryType: 'Point' | 'LineString' | 'Polygon', layerUuid: string): Promise<void> {
499
+ this.disableSelection();
500
+
501
+ if (this.drawInteraction) {
502
+ this.olMap.removeInteraction(this.drawInteraction);
503
+ }
504
+
505
+ this.activeDrawingLayerUuid = layerUuid;
506
+ this.setCursor('crosshair');
507
+
508
+ const layers = this.olMap.getLayers();
509
+ let layer: BaseLayer | undefined;
510
+ for (let i = 0; i < layers.getLength(); i++) {
511
+ const l = layers.item(i);
512
+ if (l.get(KEY_UUID) === layerUuid) {
513
+ layer = l;
514
+ break;
515
+ }
516
+ }
517
+
518
+ if (!layer || !(layer instanceof layerNS.Vector)) {
519
+ throw new Error('Drawing only supported on vector layers');
520
+ }
521
+
522
+ const source = layer.getSource();
523
+ if (!source) {
524
+ throw new Error('Layer has no source');
525
+ }
526
+
527
+ const layerSourceType = layer.get('sourceType');
528
+ if (layerSourceType && layerSourceType !== GsSourceType.Features) {
529
+ throw new Error('Drawing only supported on layers with in-memory features');
530
+ }
531
+
532
+ this.drawInteraction = new interactionNS.Draw({
533
+ source: source,
534
+ type: geometryType
535
+ });
536
+
537
+ const onFeatureAdded = (event: any) => {
538
+ const feature = event.feature;
539
+ if (feature && !feature.get(KEY_UUID)) {
540
+ const uuid = uuidv4();
541
+ feature.set(KEY_UUID, uuid);
542
+ const state = feature.get(KEY_STATE) || {};
543
+ state.uuid = uuid;
544
+ feature.set(KEY_STATE, state);
545
+ }
546
+
547
+ if (this.renderer && this.activeDrawingLayerUuid) {
548
+ this.renderer.syncLayerFeaturesToModel(this.activeDrawingLayerUuid);
549
+ }
550
+ this.renderer?.triggerDirty();
551
+ };
552
+
553
+ source.on('addfeature', onFeatureAdded);
554
+
555
+ (this.drawInteraction as any)._featureAddedListener = onFeatureAdded;
556
+ (this.drawInteraction as any)._sourceRef = source;
557
+
558
+ this.olMap.addInteraction(this.drawInteraction);
559
+ }
560
+
561
+ async disableDrawing(): Promise<void> {
562
+ if (this.drawInteraction) {
563
+ const listener = (this.drawInteraction as any)._featureAddedListener;
564
+ const source = (this.drawInteraction as any)._sourceRef;
565
+ if (listener && source) {
566
+ source.un('addfeature', listener);
567
+ }
568
+
569
+ this.olMap.removeInteraction(this.drawInteraction);
570
+ this.drawInteraction = undefined;
571
+ this.setCursor('');
572
+ }
573
+ }
574
+
575
+ cleanup(): void {
576
+ if (this.keyDownListener) {
577
+ const target = this.olMap.getTargetElement();
578
+ if (target && target instanceof HTMLElement) {
579
+ target.removeEventListener('keydown', this.keyDownListener);
580
+ }
581
+ this.keyDownListener = undefined;
582
+ }
583
+ }
584
+
585
+ async enableFeatureSelection(): Promise<void> {
586
+ this.disableDrawing();
587
+ this.disableSelection();
588
+
589
+ const olLayers = this.olMap.getLayers();
590
+ const vectorLayers = olLayers.getArray().filter(layer => layer instanceof layerNS.Vector) as layerNS.Vector<sourceNS.Vector>[];
591
+
592
+ if (vectorLayers.length === 0) {
593
+ throw new Error('No vector layers available for selection');
594
+ }
595
+
596
+ const gsMap = this.renderer?.gsMap;
597
+ const selectionStyle = gsMap?.styles?.['selection'];
598
+
599
+ const selectOptions: any = {
600
+ condition: eventsCondition.click,
601
+ layers: vectorLayers,
602
+ hitTolerance: 5,
603
+ style: selectionStyle ? toOlStyle(selectionStyle) : (_feature: any) => {
604
+ const stroke = new style.Stroke({ color: 'rgba(255, 255, 0, 1)', width: 3 });
605
+ const fill = new style.Fill({ color: 'rgba(255, 255, 0, 0.3)' });
606
+ return new style.Style({
607
+ image: new style.Circle({ radius: 7, fill: fill, stroke: stroke }),
608
+ stroke: stroke,
609
+ fill: fill
610
+ });
611
+ }
612
+ };
613
+
614
+ this.selectInteraction = new interactionNS.Select(selectOptions);
615
+
616
+ this.selectInteraction.on('select', (event: any) => {
617
+ if (event.selected.length > 0) {
618
+ const selectedFeature = event.selected[0];
619
+ let featureLayerUuid: string | undefined;
620
+ olLayers.getArray().forEach((layer) => {
621
+ if (layer instanceof layerNS.Vector) {
622
+ const source = layer.getSource();
623
+ if (source && source.hasFeature(selectedFeature)) {
624
+ const uuid = layer.get(KEY_UUID);
625
+ if (uuid) featureLayerUuid = uuid;
626
+ }
627
+ }
628
+ });
629
+
630
+ if (featureLayerUuid && this.renderer) {
631
+ const gsFeature = toGsFeature(selectedFeature);
632
+ const geometry = selectedFeature.getGeometry();
633
+ const metrics: { length?: number, area?: number } = {};
634
+
635
+ if (geometry) {
636
+ const geometryType = geometry.getType();
637
+ try {
638
+ if (geometryType === 'LineString' || geometryType === 'MultiLineString') {
639
+ metrics.length = sphere.getLength(geometry, { projection: this.olMap.getView().getProjection() });
640
+ } else if (geometryType === 'Polygon' || geometryType === 'MultiPolygon') {
641
+ metrics.area = sphere.getArea(geometry, { projection: this.olMap.getView().getProjection() });
642
+ const coordinates = geometryType === 'Polygon'
643
+ ? (geometry as any).getCoordinates()[0]
644
+ : (geometry as any).getCoordinates()[0][0];
645
+ if (coordinates?.length > 0) {
646
+ const perimeterLine = new geom.LineString(coordinates);
647
+ metrics.length = sphere.getLength(perimeterLine, { projection: this.olMap.getView().getProjection() });
648
+ }
649
+ }
650
+ } catch (error) {
651
+ console.warn('Error calculating feature metrics:', error);
652
+ }
653
+ }
654
+
655
+ this.renderer.triggerSync({
656
+ type: 'featureSelected',
657
+ layerUuid: featureLayerUuid,
658
+ feature: gsFeature,
659
+ metrics
660
+ });
661
+ }
662
+ } else if (event.deselected.length > 0) {
663
+ this.renderer?.triggerSync({ type: 'featureDeselected' });
664
+ }
665
+ });
666
+
667
+ this.olMap.addInteraction(this.selectInteraction);
668
+ this.setCursor('pointer');
669
+ }
670
+
671
+ async deleteSelectedFeatures(): Promise<void> {
672
+ if (!this.selectInteraction) {
673
+ throw new Error('No selection interaction active');
674
+ }
675
+
676
+ const selectedFeatures = this.selectInteraction.getFeatures();
677
+
678
+ if (selectedFeatures.getLength() === 0) {
679
+ throw new Error('No features selected');
680
+ }
681
+
682
+ const layersToSync = new Set<string>();
683
+ const olLayers = this.olMap.getLayers();
684
+
685
+ selectedFeatures.forEach((feature: any) => {
686
+ for (let i = 0; i < olLayers.getLength(); i++) {
687
+ const layer = olLayers.item(i);
688
+ if (layer instanceof layerNS.Vector) {
689
+ const source = layer.getSource();
690
+ if (source && source.hasFeature(feature)) {
691
+ source.removeFeature(feature);
692
+ const layerUuid = layer.get(KEY_UUID);
693
+ if (layerUuid) layersToSync.add(layerUuid);
694
+ break;
695
+ }
696
+ }
697
+ }
698
+ });
699
+
700
+ selectedFeatures.clear();
701
+
702
+ if (this.renderer && layersToSync.size > 0) {
703
+ layersToSync.forEach(layerUuid => {
704
+ this.renderer!.syncLayerFeaturesToModel(layerUuid);
705
+ });
706
+ }
707
+ this.renderer?.triggerDirty();
708
+ }
709
+
710
+ async disableSelection(): Promise<void> {
711
+ if (this.selectInteraction) {
712
+ this.olMap.removeInteraction(this.selectInteraction);
713
+ this.selectInteraction = undefined;
714
+ this.setCursor('');
715
+ this.renderer?.triggerSync({ type: 'featureDeselected' });
716
+ }
717
+ }
718
+ }
719
+