@angular-helpers/openlayers 0.4.0 → 0.5.1

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.
@@ -1,13 +1,22 @@
1
1
  import * as i0 from '@angular/core';
2
- import { signal, computed, Injectable, inject } from '@angular/core';
3
- import { OlMapService, OlZoneHelper } from '@angular-helpers/openlayers/core';
2
+ import { signal, computed, Injectable, inject, DestroyRef, input, effect, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { OlMapService, OlZoneHelper, olFeatureToFeature } from '@angular-helpers/openlayers/core';
4
4
  import { Subject } from 'rxjs';
5
5
  import Select from 'ol/interaction/Select';
6
+ import { pointerMove, click } from 'ol/events/condition';
7
+ import { Style, Icon, Fill, Stroke, Circle } from 'ol/style';
6
8
  import Draw from 'ol/interaction/Draw';
7
9
  import Snap from 'ol/interaction/Snap';
8
10
  import VectorSource from 'ol/source/Vector';
11
+ import { Polygon, LineString } from 'ol/geom';
9
12
  import { OlLayerService } from '@angular-helpers/openlayers/layers';
10
13
  import Modify from 'ol/interaction/Modify';
14
+ import VectorLayer from 'ol/layer/Vector';
15
+ import Overlay from 'ol/Overlay';
16
+ import { getLength, getArea } from 'ol/sphere';
17
+ import { unByKey } from 'ol/Observable';
18
+ import { outputFromObservable } from '@angular/core/rxjs-interop';
19
+ import { filter } from 'rxjs/operators';
11
20
 
12
21
  // Interaction state management service
13
22
  /**
@@ -18,12 +27,15 @@ class InteractionStateService {
18
27
  // Internal signals
19
28
  interactions = signal([], ...(ngDevMode ? [{ debugName: "interactions" }] : /* istanbul ignore next */ []));
20
29
  selectedFeaturesInternal = signal([], ...(ngDevMode ? [{ debugName: "selectedFeaturesInternal" }] : /* istanbul ignore next */ []));
30
+ hoveredFeatureInternal = signal(null, ...(ngDevMode ? [{ debugName: "hoveredFeatureInternal" }] : /* istanbul ignore next */ []));
21
31
  // Event subjects
22
32
  drawStartSubject = new Subject();
23
33
  drawEndSubject = new Subject();
24
34
  modifySubject = new Subject();
35
+ selectSubject = new Subject();
25
36
  // Public readonly signals
26
37
  selectedFeatures = computed(() => this.selectedFeaturesInternal(), ...(ngDevMode ? [{ debugName: "selectedFeatures" }] : /* istanbul ignore next */ []));
38
+ hoveredFeature = computed(() => this.hoveredFeatureInternal(), ...(ngDevMode ? [{ debugName: "hoveredFeature" }] : /* istanbul ignore next */ []));
27
39
  selectionCount = computed(() => this.selectedFeaturesInternal().length, ...(ngDevMode ? [{ debugName: "selectionCount" }] : /* istanbul ignore next */ []));
28
40
  hasSelection = computed(() => this.selectedFeaturesInternal().length > 0, ...(ngDevMode ? [{ debugName: "hasSelection" }] : /* istanbul ignore next */ []));
29
41
  activeInteractions = computed(() => this.interactions().filter((i) => i.config.active !== false), ...(ngDevMode ? [{ debugName: "activeInteractions" }] : /* istanbul ignore next */ []));
@@ -31,6 +43,7 @@ class InteractionStateService {
31
43
  drawStart$ = this.drawStartSubject.asObservable();
32
44
  drawEnd$ = this.drawEndSubject.asObservable();
33
45
  modify$ = this.modifySubject.asObservable();
46
+ select$ = this.selectSubject.asObservable();
34
47
  /**
35
48
  * Adds a managed interaction to the state.
36
49
  * If the interaction is marked as exclusive, it disables other exclusive interactions.
@@ -84,6 +97,13 @@ class InteractionStateService {
84
97
  clearSelection() {
85
98
  this.selectedFeaturesInternal.set([]);
86
99
  }
100
+ /**
101
+ * Sets the currently hovered feature.
102
+ * @param feature - The hovered feature or null
103
+ */
104
+ setHoveredFeature(feature) {
105
+ this.hoveredFeatureInternal.set(feature);
106
+ }
87
107
  /**
88
108
  * Emits a draw start event.
89
109
  * @param event - The draw start event data
@@ -105,6 +125,13 @@ class InteractionStateService {
105
125
  emitModify(event) {
106
126
  this.modifySubject.next(event);
107
127
  }
128
+ /**
129
+ * Emits a select event.
130
+ * @param event - The select event data
131
+ */
132
+ emitSelect(event) {
133
+ this.selectSubject.next(event);
134
+ }
108
135
  /**
109
136
  * Gets the current interaction state summary.
110
137
  * @returns Array of interaction state objects
@@ -132,54 +159,15 @@ class InteractionStateService {
132
159
  clearAll() {
133
160
  this.interactions.set([]);
134
161
  this.selectedFeaturesInternal.set([]);
162
+ this.hoveredFeatureInternal.set(null);
135
163
  }
136
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: InteractionStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
137
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: InteractionStateService });
164
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: InteractionStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
165
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: InteractionStateService });
138
166
  }
139
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: InteractionStateService, decorators: [{
167
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: InteractionStateService, decorators: [{
140
168
  type: Injectable
141
169
  }] });
142
170
 
143
- // Feature conversion utilities for OpenLayers interactions
144
- /**
145
- * Converts an OpenLayers feature to the internal Feature format.
146
- * Handles coordinate extraction and geometry type mapping.
147
- *
148
- * @param olFeature - The OpenLayers feature to convert
149
- * @returns The converted Feature with normalized structure
150
- */
151
- function olFeatureToFeature(olFeature) {
152
- const geometry = olFeature.getGeometry();
153
- const geomType = geometry?.getType() ?? 'Point';
154
- // Convert coordinates based on geometry type
155
- let coordinates;
156
- if (geomType === 'Circle') {
157
- // ol/geom/Circle has no getCoordinates() — use getCenter() instead
158
- const circle = geometry;
159
- coordinates = circle.getCenter();
160
- }
161
- else {
162
- // oxlint-disable-next-line no-explicit-any
163
- const olCoords = geometry.getCoordinates();
164
- if (Array.isArray(olCoords) && Array.isArray(olCoords[0])) {
165
- // Multi-coordinate structures (LineString, Polygon, etc.)
166
- coordinates = olCoords;
167
- }
168
- else {
169
- // Single point
170
- coordinates = olCoords;
171
- }
172
- }
173
- return {
174
- id: olFeature.getId()?.toString() ?? `feature-${Math.random().toString(36).slice(2)}`,
175
- geometry: {
176
- type: geomType,
177
- coordinates,
178
- },
179
- properties: olFeature.getProperties(),
180
- };
181
- }
182
-
183
171
  // Select interaction creation service
184
172
  /**
185
173
  * Service responsible for creating and managing Select interactions.
@@ -206,17 +194,99 @@ class SelectInteractionService {
206
194
  : undefined,
207
195
  multi: config.multi ?? false,
208
196
  hitTolerance: config.hitTolerance ?? 0,
197
+ condition: config.condition === 'pointerMove' ? pointerMove : click,
198
+ style: (olFeature, resolution) => {
199
+ const styles = [];
200
+ // 1. Resolve feature's own custom style (same as layer style Fn)
201
+ const abstractStyle = olFeature.get('__angular_helpers_style__');
202
+ const featureStyle = new Style();
203
+ let hasCustom = false;
204
+ if (abstractStyle) {
205
+ const { icon, fill, stroke } = abstractStyle;
206
+ if (icon?.src) {
207
+ featureStyle.setImage(new Icon({
208
+ src: icon.src,
209
+ ...(icon.size ? { size: icon.size } : {}),
210
+ ...(icon.anchor ? { anchor: icon.anchor } : {}),
211
+ }));
212
+ hasCustom = true;
213
+ }
214
+ if (fill) {
215
+ featureStyle.setFill(new Fill({ color: fill.color }));
216
+ hasCustom = true;
217
+ }
218
+ if (stroke) {
219
+ featureStyle.setStroke(new Stroke({ color: stroke.color, width: stroke.width }));
220
+ hasCustom = true;
221
+ }
222
+ }
223
+ if (!hasCustom) {
224
+ // Fallback to standard vector default style
225
+ featureStyle.setFill(new Fill({ color: 'rgba(25, 118, 210, 0.3)' }));
226
+ featureStyle.setStroke(new Stroke({ color: '#1976d2', width: 2 }));
227
+ featureStyle.setImage(new Circle({
228
+ radius: 8,
229
+ fill: new Fill({ color: '#1976d2' }),
230
+ stroke: new Stroke({ color: '#d32f2f', width: 2 }),
231
+ }));
232
+ }
233
+ styles.push(featureStyle);
234
+ // 2. Add high-fidelity premium selection highlight overlay
235
+ const geometry = olFeature.getGeometry();
236
+ if (geometry) {
237
+ const geomType = geometry.getType();
238
+ if (geomType === 'Point') {
239
+ // Outer glowing selection ring for Points
240
+ styles.push(new Style({
241
+ image: new Circle({
242
+ radius: 12,
243
+ stroke: new Stroke({
244
+ color: 'rgba(59, 130, 246, 0.8)',
245
+ width: 2,
246
+ lineDash: [4, 4],
247
+ }),
248
+ }),
249
+ }));
250
+ }
251
+ else {
252
+ // Outer thick dashed highlight for Polygons/LineStrings
253
+ styles.push(new Style({
254
+ stroke: new Stroke({
255
+ color: 'rgba(59, 130, 246, 0.9)',
256
+ width: 3,
257
+ lineDash: [6, 4],
258
+ }),
259
+ fill: new Fill({
260
+ color: 'rgba(59, 130, 246, 0.05)',
261
+ }),
262
+ }));
263
+ }
264
+ }
265
+ return styles;
266
+ },
209
267
  });
210
268
  // Listen to selection changes — use getFeatures().getArray() for the full
211
269
  // accumulated collection, not e.selected which only contains newly added ones
212
- select.on('select', (_e) => {
213
- this.zoneHelper.runInsideAngular(() => {
214
- const allSelected = select
215
- .getFeatures()
216
- .getArray()
217
- .map((f) => olFeatureToFeature(f));
218
- this.stateService.setSelectedFeatures(allSelected);
219
- });
270
+ select.on('select', (e) => {
271
+ const allFeatures = select
272
+ .getFeatures()
273
+ .getArray()
274
+ .map((f) => olFeatureToFeature(f));
275
+ if (config.condition === 'pointerMove') {
276
+ // Update signal outside Angular zone. Consumers of this signal will schedule
277
+ // targeted change detection automatically.
278
+ this.stateService.setHoveredFeature(allFeatures.length > 0 ? allFeatures[0] : null);
279
+ }
280
+ else {
281
+ this.zoneHelper.runInsideAngular(() => {
282
+ this.stateService.setSelectedFeatures(allFeatures);
283
+ this.stateService.emitSelect({
284
+ interactionId: id,
285
+ selected: e.selected.map((f) => olFeatureToFeature(f)),
286
+ deselected: e.deselected.map((f) => olFeatureToFeature(f)),
287
+ });
288
+ });
289
+ }
220
290
  });
221
291
  map.addInteraction(select);
222
292
  const managed = {
@@ -232,10 +302,10 @@ class SelectInteractionService {
232
302
  this.stateService.addInteraction(managed);
233
303
  });
234
304
  }
235
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SelectInteractionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
236
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SelectInteractionService });
305
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SelectInteractionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
306
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SelectInteractionService });
237
307
  }
238
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SelectInteractionService, decorators: [{
308
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SelectInteractionService, decorators: [{
239
309
  type: Injectable
240
310
  }] });
241
311
 
@@ -267,9 +337,69 @@ class DrawInteractionService {
267
337
  source = new VectorSource();
268
338
  }
269
339
  this.zoneHelper.runOutsideAngular(() => {
340
+ let drawType = config.type;
341
+ let geometryFunction;
342
+ if (config.type === 'Ellipse') {
343
+ drawType = 'Circle';
344
+ geometryFunction = (coords, geom) => {
345
+ if (!geom) {
346
+ geom = new Polygon([]);
347
+ }
348
+ const center = coords[0];
349
+ const last = coords[1];
350
+ const dx = last[0] - center[0];
351
+ const dy = last[1] - center[1];
352
+ const semiMajor = Math.sqrt(dx * dx + dy * dy);
353
+ const semiMinor = semiMajor * 0.7; // Default ratio
354
+ const rotation = Math.atan2(dy, dx);
355
+ const ring = [];
356
+ const segments = 64;
357
+ for (let i = 0; i < segments; i++) {
358
+ const theta = (i / segments) * Math.PI * 2;
359
+ const ax = Math.cos(theta) * semiMajor;
360
+ const ay = Math.sin(theta) * semiMinor;
361
+ const rx = ax * Math.cos(rotation) - ay * Math.sin(rotation);
362
+ const ry = ax * Math.sin(rotation) + ay * Math.cos(rotation);
363
+ ring.push([center[0] + rx, center[1] + ry]);
364
+ }
365
+ ring.push(ring[0]);
366
+ geom.setCoordinates([ring]);
367
+ return geom;
368
+ };
369
+ }
370
+ else if (config.type === 'Donut') {
371
+ drawType = 'Circle';
372
+ geometryFunction = (coords, geom) => {
373
+ if (!geom) {
374
+ geom = new Polygon([]);
375
+ }
376
+ const center = coords[0];
377
+ const last = coords[1];
378
+ const dx = last[0] - center[0];
379
+ const dy = last[1] - center[1];
380
+ const outerRadius = Math.sqrt(dx * dx + dy * dy);
381
+ const innerRadius = outerRadius * 0.6; // Default ratio
382
+ const outer = [];
383
+ const inner = [];
384
+ const segments = 64;
385
+ for (let i = 0; i < segments; i++) {
386
+ const theta = (i / segments) * Math.PI * 2;
387
+ const cosT = Math.cos(theta);
388
+ const sinT = Math.sin(theta);
389
+ outer.push([center[0] + cosT * outerRadius, center[1] + sinT * outerRadius]);
390
+ inner.push([center[0] + cosT * innerRadius, center[1] + sinT * innerRadius]);
391
+ }
392
+ inner.reverse();
393
+ outer.push(outer[0]);
394
+ inner.push(inner[0]);
395
+ geom.setCoordinates([outer, inner]);
396
+ return geom;
397
+ };
398
+ }
270
399
  const draw = new Draw({
271
400
  source,
272
- type: config.type,
401
+ type: drawType,
402
+ geometryFunction,
273
403
  freehand: config.freehand ?? false,
274
404
  snapTolerance: config.snapTolerance ?? 12,
275
405
  });
@@ -278,14 +408,34 @@ class DrawInteractionService {
278
408
  draw.on('drawstart', (e) => {
279
409
  this.zoneHelper.runInsideAngular(() => {
280
410
  const feature = olFeatureToFeature(e.feature);
281
- this.stateService.emitDrawStart({ feature });
411
+ this.stateService.emitDrawStart({ interactionId: id, feature });
282
412
  });
283
413
  });
284
414
  // Handle draw end event
285
415
  draw.on('drawend', (e) => {
286
416
  this.zoneHelper.runInsideAngular(() => {
417
+ const uniqueId = `drawn-${Date.now()}`;
418
+ e.feature.setId(uniqueId);
419
+ // Set default properties and styling on raw OL feature
420
+ e.feature.set('name', 'Sketch');
421
+ const defaultStyle = {
422
+ fill: { color: 'rgba(59, 130, 246, 0.2)' },
423
+ stroke: { color: '#3b82f6', width: 2 },
424
+ };
425
+ e.feature.set('__angular_helpers_style__', defaultStyle);
426
+ // Convert to internal Feature representation
287
427
  const feature = olFeatureToFeature(e.feature);
288
- const event = { feature, type: config.type };
428
+ feature.id = uniqueId;
429
+ feature.properties = {
430
+ ...feature.properties,
431
+ name: 'Sketch',
432
+ strokeColor: '#3b82f6',
433
+ strokeWidth: 2,
434
+ fillColor: '#3b82f6',
435
+ fillOpacity: 0.2,
436
+ };
437
+ feature.style = defaultStyle;
438
+ const event = { interactionId: id, feature, type: config.type };
289
439
  this.stateService.emitDrawEnd(event);
290
440
  });
291
441
  });
@@ -307,10 +457,10 @@ class DrawInteractionService {
307
457
  });
308
458
  return true;
309
459
  }
310
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: DrawInteractionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
311
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: DrawInteractionService });
460
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: DrawInteractionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
461
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: DrawInteractionService });
312
462
  }
313
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: DrawInteractionService, decorators: [{
463
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: DrawInteractionService, decorators: [{
314
464
  type: Injectable
315
465
  }] });
316
466
 
@@ -348,7 +498,7 @@ class ModifyInteractionService {
348
498
  modify.on('modifystart', (e) => {
349
499
  this.zoneHelper.runInsideAngular(() => {
350
500
  const features = e.features.getArray().map((f) => olFeatureToFeature(f));
351
- const event = { features, type: 'modifystart' };
501
+ const event = { interactionId: id, features, type: 'modifystart' };
352
502
  this.stateService.emitModify(event);
353
503
  });
354
504
  });
@@ -356,7 +506,7 @@ class ModifyInteractionService {
356
506
  modify.on('modifyend', (e) => {
357
507
  this.zoneHelper.runInsideAngular(() => {
358
508
  const features = e.features.getArray().map((f) => olFeatureToFeature(f));
359
- const event = { features, type: 'modifyend' };
509
+ const event = { interactionId: id, features, type: 'modifyend' };
360
510
  this.stateService.emitModify(event);
361
511
  });
362
512
  });
@@ -377,10 +527,10 @@ class ModifyInteractionService {
377
527
  this.stateService.addInteraction(managed);
378
528
  });
379
529
  }
380
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ModifyInteractionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
381
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ModifyInteractionService });
530
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ModifyInteractionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
531
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ModifyInteractionService });
382
532
  }
383
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: ModifyInteractionService, decorators: [{
533
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ModifyInteractionService, decorators: [{
384
534
  type: Injectable
385
535
  }] });
386
536
 
@@ -413,12 +563,14 @@ class OlInteractionService {
413
563
  modifyService = inject(ModifyInteractionService);
414
564
  // Public signals and observables delegated to state service
415
565
  selectedFeatures = this.stateService.selectedFeatures;
566
+ hoveredFeature = this.stateService.hoveredFeature;
416
567
  selectionCount = this.stateService.selectionCount;
417
568
  hasSelection = this.stateService.hasSelection;
418
569
  activeInteractions = this.stateService.activeInteractions;
419
570
  drawStart$ = this.stateService.drawStart$;
420
571
  drawEnd$ = this.stateService.drawEnd$;
421
572
  modify$ = this.stateService.modify$;
573
+ select$ = this.stateService.select$;
422
574
  /**
423
575
  * Enable a select interaction on the map.
424
576
  * @param id - Unique identifier for this interaction
@@ -528,13 +680,305 @@ class OlInteractionService {
528
680
  getInteractionState() {
529
681
  return this.stateService.getInteractionState();
530
682
  }
531
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlInteractionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
532
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlInteractionService });
683
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlInteractionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
684
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlInteractionService });
685
+ }
686
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlInteractionService, decorators: [{
687
+ type: Injectable
688
+ }] });
689
+
690
+ class MeasurementInteractionService {
691
+ mapService = inject(OlMapService);
692
+ zoneHelper = inject(OlZoneHelper);
693
+ draw;
694
+ source = new VectorSource();
695
+ vectorLayer;
696
+ map;
697
+ sketch = null;
698
+ measureTooltipElement = null;
699
+ measureTooltip = null;
700
+ listener = null;
701
+ isMeasuring = false;
702
+ startMeasuring(type) {
703
+ if (this.isMeasuring)
704
+ this.stopMeasuring();
705
+ this.map = this.mapService.getMap() ?? undefined;
706
+ if (!this.map)
707
+ return;
708
+ this.zoneHelper.runOutsideAngular(() => {
709
+ this.vectorLayer = new VectorLayer({
710
+ source: this.source,
711
+ style: new Style({
712
+ fill: new Fill({ color: 'rgba(255, 255, 255, 0.2)' }),
713
+ stroke: new Stroke({ color: '#ffcc33', width: 2 }),
714
+ image: new Circle({ radius: 7, fill: new Fill({ color: '#ffcc33' }) }),
715
+ }),
716
+ zIndex: 9999,
717
+ });
718
+ this.map.addLayer(this.vectorLayer);
719
+ this.draw = new Draw({
720
+ source: this.source,
721
+ type,
722
+ style: new Style({
723
+ fill: new Fill({ color: 'rgba(255, 255, 255, 0.2)' }),
724
+ stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.5)', lineDash: [10, 10], width: 2 }),
725
+ image: new Circle({
726
+ radius: 5,
727
+ stroke: new Stroke({ color: 'rgba(0, 0, 0, 0.7)' }),
728
+ fill: new Fill({ color: 'rgba(255, 255, 255, 0.2)' }),
729
+ }),
730
+ }),
731
+ });
732
+ this.map.addInteraction(this.draw);
733
+ this.createMeasureTooltip();
734
+ this.draw.on('drawstart', (evt) => {
735
+ this.sketch = evt.feature;
736
+ let tooltipCoord = evt.coordinate;
737
+ const geometry = this.sketch?.getGeometry();
738
+ if (geometry) {
739
+ this.listener = geometry.on('change', (e) => {
740
+ const geom = e.target;
741
+ let output;
742
+ if (geom instanceof Polygon) {
743
+ output = this.formatArea(geom);
744
+ tooltipCoord = geom.getInteriorPoint().getCoordinates();
745
+ }
746
+ else if (geom instanceof LineString) {
747
+ output = this.formatLength(geom);
748
+ tooltipCoord = geom.getLastCoordinate();
749
+ }
750
+ else {
751
+ output = '';
752
+ }
753
+ if (this.measureTooltipElement) {
754
+ this.measureTooltipElement.innerHTML = output;
755
+ this.measureTooltip?.setPosition(tooltipCoord);
756
+ }
757
+ });
758
+ }
759
+ });
760
+ this.draw.on('drawend', () => {
761
+ if (this.measureTooltipElement) {
762
+ this.measureTooltipElement.className = 'ol-tooltip ol-tooltip-static';
763
+ this.measureTooltip?.setOffset([0, -7]);
764
+ this.sketch = null;
765
+ this.measureTooltipElement = null;
766
+ this.createMeasureTooltip();
767
+ }
768
+ if (this.listener)
769
+ unByKey(this.listener);
770
+ });
771
+ this.isMeasuring = true;
772
+ });
773
+ }
774
+ stopMeasuring() {
775
+ if (!this.isMeasuring)
776
+ return;
777
+ this.zoneHelper.runOutsideAngular(() => {
778
+ if (this.draw && this.map)
779
+ this.map.removeInteraction(this.draw);
780
+ if (this.vectorLayer && this.map)
781
+ this.map.removeLayer(this.vectorLayer);
782
+ this.source.clear();
783
+ // Remove all tooltips
784
+ const overlays = this.map
785
+ ?.getOverlays()
786
+ .getArray()
787
+ .filter((o) => o.getElement()?.classList.contains('ol-tooltip'));
788
+ overlays?.forEach((o) => this.map?.removeOverlay(o));
789
+ this.isMeasuring = false;
790
+ this.measureTooltipElement = null;
791
+ this.measureTooltip = null;
792
+ });
793
+ }
794
+ isActive() {
795
+ return this.isMeasuring;
796
+ }
797
+ createMeasureTooltip() {
798
+ if (!this.map)
799
+ return;
800
+ if (this.measureTooltipElement) {
801
+ this.measureTooltipElement.parentNode?.removeChild(this.measureTooltipElement);
802
+ }
803
+ this.measureTooltipElement = document.createElement('div');
804
+ this.measureTooltipElement.className = 'ol-tooltip ol-tooltip-measure';
805
+ this.measureTooltipElement.style.backgroundColor = 'rgba(0,0,0,0.7)';
806
+ this.measureTooltipElement.style.color = 'white';
807
+ this.measureTooltipElement.style.padding = '4px 8px';
808
+ this.measureTooltipElement.style.borderRadius = '4px';
809
+ this.measureTooltipElement.style.fontSize = '12px';
810
+ this.measureTooltipElement.style.whiteSpace = 'nowrap';
811
+ this.measureTooltipElement.style.pointerEvents = 'none';
812
+ this.measureTooltip = new Overlay({
813
+ element: this.measureTooltipElement,
814
+ offset: [0, -15],
815
+ positioning: 'bottom-center',
816
+ stopEvent: false,
817
+ });
818
+ this.map.addOverlay(this.measureTooltip);
819
+ }
820
+ formatLength(line) {
821
+ const length = getLength(line);
822
+ let output;
823
+ if (length > 100) {
824
+ output = Math.round((length / 1000) * 100) / 100 + ' km';
825
+ }
826
+ else {
827
+ output = Math.round(length * 100) / 100 + ' m';
828
+ }
829
+ return output;
830
+ }
831
+ formatArea(polygon) {
832
+ const area = getArea(polygon);
833
+ let output;
834
+ if (area > 10000) {
835
+ output = Math.round((area / 1000000) * 100) / 100 + ' km<sup>2</sup>';
836
+ }
837
+ else {
838
+ output = Math.round(area * 100) / 100 + ' m<sup>2</sup>';
839
+ }
840
+ return output;
841
+ }
842
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MeasurementInteractionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
843
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MeasurementInteractionService });
533
844
  }
534
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlInteractionService, decorators: [{
845
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MeasurementInteractionService, decorators: [{
535
846
  type: Injectable
536
847
  }] });
537
848
 
849
+ /**
850
+ * Declarative component to configure an OpenLayers Draw Interaction.
851
+ */
852
+ class OlDrawInteractionComponent {
853
+ interactionService = inject(OlInteractionService);
854
+ destroyRef = inject(DestroyRef);
855
+ id = input.required(...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
856
+ type = input.required(...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
857
+ source = input(...(ngDevMode ? [undefined, { debugName: "source" }] : /* istanbul ignore next */ []));
858
+ freehand = input(...(ngDevMode ? [undefined, { debugName: "freehand" }] : /* istanbul ignore next */ []));
859
+ snapTolerance = input(...(ngDevMode ? [undefined, { debugName: "snapTolerance" }] : /* istanbul ignore next */ []));
860
+ active = input(true, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
861
+ drawStartFiltered$ = this.interactionService.drawStart$.pipe(filter((e) => e.interactionId === this.id()));
862
+ drawEndFiltered$ = this.interactionService.drawEnd$.pipe(filter((e) => e.interactionId === this.id()));
863
+ drawStart = outputFromObservable(this.drawStartFiltered$);
864
+ drawEnd = outputFromObservable(this.drawEndFiltered$);
865
+ constructor() {
866
+ effect(() => {
867
+ if (this.active()) {
868
+ this.interactionService.enableDraw(this.id(), {
869
+ type: this.type(),
870
+ source: this.source(),
871
+ freehand: this.freehand(),
872
+ snapTolerance: this.snapTolerance(),
873
+ });
874
+ }
875
+ else {
876
+ this.interactionService.disableInteraction(this.id());
877
+ }
878
+ });
879
+ this.destroyRef.onDestroy(() => {
880
+ this.interactionService.disableInteraction(this.id());
881
+ });
882
+ }
883
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlDrawInteractionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
884
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: OlDrawInteractionComponent, isStandalone: true, selector: "ol-draw-interaction", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: true, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, freehand: { classPropertyName: "freehand", publicName: "freehand", isSignal: true, isRequired: false, transformFunction: null }, snapTolerance: { classPropertyName: "snapTolerance", publicName: "snapTolerance", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { drawStart: "drawStart", drawEnd: "drawEnd" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
885
+ }
886
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlDrawInteractionComponent, decorators: [{
887
+ type: Component,
888
+ args: [{
889
+ selector: 'ol-draw-interaction',
890
+ standalone: true,
891
+ template: '',
892
+ changeDetection: ChangeDetectionStrategy.OnPush,
893
+ }]
894
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: true }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], freehand: [{ type: i0.Input, args: [{ isSignal: true, alias: "freehand", required: false }] }], snapTolerance: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapTolerance", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], drawStart: [{ type: i0.Output, args: ["drawStart"] }], drawEnd: [{ type: i0.Output, args: ["drawEnd"] }] } });
895
+
896
+ /**
897
+ * Declarative component to configure an OpenLayers Modify Interaction.
898
+ */
899
+ class OlModifyInteractionComponent {
900
+ interactionService = inject(OlInteractionService);
901
+ destroyRef = inject(DestroyRef);
902
+ id = input.required(...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
903
+ source = input(...(ngDevMode ? [undefined, { debugName: "source" }] : /* istanbul ignore next */ []));
904
+ snapTolerance = input(...(ngDevMode ? [undefined, { debugName: "snapTolerance" }] : /* istanbul ignore next */ []));
905
+ active = input(true, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
906
+ modifyFiltered$ = this.interactionService.modify$.pipe(filter((e) => e.interactionId === this.id()));
907
+ modifyEvent = outputFromObservable(this.modifyFiltered$);
908
+ constructor() {
909
+ effect(() => {
910
+ if (this.active()) {
911
+ this.interactionService.enableModify(this.id(), {
912
+ source: this.source(),
913
+ snapTolerance: this.snapTolerance(),
914
+ });
915
+ }
916
+ else {
917
+ this.interactionService.disableInteraction(this.id());
918
+ }
919
+ });
920
+ this.destroyRef.onDestroy(() => {
921
+ this.interactionService.disableInteraction(this.id());
922
+ });
923
+ }
924
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlModifyInteractionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
925
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: OlModifyInteractionComponent, isStandalone: true, selector: "ol-modify-interaction", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, snapTolerance: { classPropertyName: "snapTolerance", publicName: "snapTolerance", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { modifyEvent: "modifyEvent" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
926
+ }
927
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlModifyInteractionComponent, decorators: [{
928
+ type: Component,
929
+ args: [{
930
+ selector: 'ol-modify-interaction',
931
+ standalone: true,
932
+ template: '',
933
+ changeDetection: ChangeDetectionStrategy.OnPush,
934
+ }]
935
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], snapTolerance: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapTolerance", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], modifyEvent: [{ type: i0.Output, args: ["modifyEvent"] }] } });
936
+
937
+ /**
938
+ * Declarative component to configure an OpenLayers Select Interaction.
939
+ */
940
+ class OlSelectInteractionComponent {
941
+ interactionService = inject(OlInteractionService);
942
+ destroyRef = inject(DestroyRef);
943
+ id = input.required(...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
944
+ layers = input(...(ngDevMode ? [undefined, { debugName: "layers" }] : /* istanbul ignore next */ []));
945
+ multi = input(...(ngDevMode ? [undefined, { debugName: "multi" }] : /* istanbul ignore next */ []));
946
+ hitTolerance = input(...(ngDevMode ? [undefined, { debugName: "hitTolerance" }] : /* istanbul ignore next */ []));
947
+ condition = input(...(ngDevMode ? [undefined, { debugName: "condition" }] : /* istanbul ignore next */ []));
948
+ active = input(true, ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
949
+ selectFiltered$ = this.interactionService.select$.pipe(filter((e) => e.interactionId === this.id()));
950
+ selectEvent = outputFromObservable(this.selectFiltered$);
951
+ constructor() {
952
+ effect(() => {
953
+ if (this.active()) {
954
+ this.interactionService.enableSelect(this.id(), {
955
+ layers: this.layers(),
956
+ multi: this.multi(),
957
+ hitTolerance: this.hitTolerance(),
958
+ condition: this.condition(),
959
+ });
960
+ }
961
+ else {
962
+ this.interactionService.disableInteraction(this.id());
963
+ }
964
+ });
965
+ this.destroyRef.onDestroy(() => {
966
+ this.interactionService.disableInteraction(this.id());
967
+ });
968
+ }
969
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlSelectInteractionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
970
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: OlSelectInteractionComponent, isStandalone: true, selector: "ol-select-interaction", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, layers: { classPropertyName: "layers", publicName: "layers", isSignal: true, isRequired: false, transformFunction: null }, multi: { classPropertyName: "multi", publicName: "multi", isSignal: true, isRequired: false, transformFunction: null }, hitTolerance: { classPropertyName: "hitTolerance", publicName: "hitTolerance", isSignal: true, isRequired: false, transformFunction: null }, condition: { classPropertyName: "condition", publicName: "condition", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectEvent: "selectEvent" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
971
+ }
972
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlSelectInteractionComponent, decorators: [{
973
+ type: Component,
974
+ args: [{
975
+ selector: 'ol-select-interaction',
976
+ standalone: true,
977
+ template: '',
978
+ changeDetection: ChangeDetectionStrategy.OnPush,
979
+ }]
980
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], layers: [{ type: i0.Input, args: [{ isSignal: true, alias: "layers", required: false }] }], multi: [{ type: i0.Input, args: [{ isSignal: true, alias: "multi", required: false }] }], hitTolerance: [{ type: i0.Input, args: [{ isSignal: true, alias: "hitTolerance", required: false }] }], condition: [{ type: i0.Input, args: [{ isSignal: true, alias: "condition", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], selectEvent: [{ type: i0.Output, args: ["selectEvent"] }] } });
981
+
538
982
  /**
539
983
  * Provide the interactions feature with OlInteractionService and all specialized services.
540
984
  * Note: ZoneHelper is inherited from core's provideOpenLayers.
@@ -550,6 +994,7 @@ function withInteractions() {
550
994
  SelectInteractionService,
551
995
  DrawInteractionService,
552
996
  ModifyInteractionService,
997
+ MeasurementInteractionService,
553
998
  ],
554
999
  };
555
1000
  }
@@ -608,6 +1053,19 @@ function withModifyInteraction(id, config = {}) {
608
1053
  deps: [OlInteractionService],
609
1054
  };
610
1055
  }
1056
+ /**
1057
+ * Enable measurement interaction when providing the interactions feature.
1058
+ * @returns Provider function that enables measurement interaction
1059
+ */
1060
+ function withMeasurementInteraction() {
1061
+ return {
1062
+ provide: 'MEASUREMENT_INTERACTION_CONFIG',
1063
+ useFactory: (service) => {
1064
+ return service;
1065
+ },
1066
+ deps: [MeasurementInteractionService],
1067
+ };
1068
+ }
611
1069
 
612
1070
  // @angular-helpers/openlayers/interactions
613
1071
  // Main orchestrator service
@@ -616,4 +1074,4 @@ function withModifyInteraction(id, config = {}) {
616
1074
  * Generated bundle index. Do not edit.
617
1075
  */
618
1076
 
619
- export { DrawInteractionService, InteractionStateService, ModifyInteractionService, OlInteractionService, SelectInteractionService, olFeatureToFeature, provideInteractions, withDrawInteraction, withInteractions, withModifyInteraction, withSelectInteraction };
1077
+ export { DrawInteractionService, InteractionStateService, MeasurementInteractionService, ModifyInteractionService, OlDrawInteractionComponent, OlInteractionService, OlModifyInteractionComponent, OlSelectInteractionComponent, SelectInteractionService, provideInteractions, withDrawInteraction, withInteractions, withMeasurementInteraction, withModifyInteraction, withSelectInteraction };