@angular-helpers/openlayers 0.3.0 → 0.5.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.
@@ -1,8 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, NgZone, Injectable, DestroyRef, input, output, viewChild, afterNextRender, effect, ChangeDetectionStrategy, Component, makeEnvironmentProviders } from '@angular/core';
2
+ import { inject, NgZone, Injectable, signal, DestroyRef, input, output, viewChild, afterNextRender, effect, ChangeDetectionStrategy, Component, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import OLMap from 'ol/Map';
4
4
  import View from 'ol/View';
5
- import { fromLonLat, toLonLat } from 'ol/proj';
5
+ import { fromLonLat, toLonLat, get } from 'ol/proj';
6
+ import { offset } from 'ol/sphere';
7
+ import { register } from 'ol/proj/proj4';
6
8
 
7
9
  // ZoneHelperService - Handles NgZone compatibility for zoneless mode
8
10
  /**
@@ -52,10 +54,10 @@ class OlZoneHelper {
52
54
  }
53
55
  return fn();
54
56
  }
55
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlZoneHelper, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
56
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlZoneHelper });
57
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlZoneHelper, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
58
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlZoneHelper });
57
59
  }
58
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlZoneHelper, decorators: [{
60
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlZoneHelper, decorators: [{
59
61
  type: Injectable
60
62
  }] });
61
63
 
@@ -64,13 +66,22 @@ class OlMapService {
64
66
  zoneHelper = inject(OlZoneHelper);
65
67
  map = null;
66
68
  readyCallbacks = [];
69
+ _resolution = signal(1, ...(ngDevMode ? [{ debugName: "_resolution" }] : /* istanbul ignore next */ []));
70
+ /** Signal that emits the current map resolution in meters per pixel */
71
+ resolution = this._resolution.asReadonly();
67
72
  setMap(map) {
68
73
  this.map = map;
69
- const callbacks = this.readyCallbacks.splice(0);
70
- for (const cb of callbacks) {
71
- cb(map);
74
+ if (map) {
75
+ this._resolution.set(map.getView()?.getResolution() ?? 1);
76
+ const callbacks = this.readyCallbacks.splice(0);
77
+ for (const cb of callbacks) {
78
+ cb(map);
79
+ }
72
80
  }
73
81
  }
82
+ setResolution(resolution) {
83
+ this._resolution.set(resolution);
84
+ }
74
85
  getMap() {
75
86
  return this.map;
76
87
  }
@@ -134,10 +145,10 @@ class OlMapService {
134
145
  rotation: view.getRotation() ?? 0,
135
146
  };
136
147
  }
137
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlMapService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
138
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlMapService });
148
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
149
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapService });
139
150
  }
140
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlMapService, decorators: [{
151
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapService, decorators: [{
141
152
  type: Injectable
142
153
  }] });
143
154
 
@@ -155,6 +166,7 @@ class OlMapComponent {
155
166
  mapDblClick = output();
156
167
  mapContainerRef = viewChild.required('mapContainer');
157
168
  map;
169
+ resizeObserver;
158
170
  constructor() {
159
171
  afterNextRender(() => this.initMap());
160
172
  effect(() => {
@@ -186,8 +198,26 @@ class OlMapComponent {
186
198
  });
187
199
  this.map = new OLMap({ target: container, view, layers: [] });
188
200
  this.mapService.setMap(this.map);
201
+ // Add ResizeObserver to handle container size changes (e.g. sidebars, window resize)
202
+ if (typeof ResizeObserver !== 'undefined') {
203
+ this.resizeObserver = new ResizeObserver(() => {
204
+ if (this.map) {
205
+ // Using requestAnimationFrame prevents "ResizeObserver loop limit exceeded" errors
206
+ requestAnimationFrame(() => {
207
+ if (this.map)
208
+ this.map.updateSize();
209
+ });
210
+ }
211
+ });
212
+ this.resizeObserver.observe(container);
213
+ }
189
214
  view.on('change:center', () => this.zoneHelper.runInsideAngular(() => this.emitViewChange()));
190
- view.on('change:resolution', () => this.zoneHelper.runInsideAngular(() => this.emitViewChange()));
215
+ view.on('change:resolution', () => {
216
+ this.zoneHelper.runInsideAngular(() => {
217
+ this.mapService.setResolution(view.getResolution() ?? 1);
218
+ this.emitViewChange();
219
+ });
220
+ });
191
221
  this.map.on('click', (e) => this.zoneHelper.runInsideAngular(() => this.mapClick.emit({
192
222
  coordinate: toLonLat(e.coordinate, this.projection()),
193
223
  pixel: e.pixel,
@@ -200,6 +230,10 @@ class OlMapComponent {
200
230
  this.emitViewChange();
201
231
  }
202
232
  destroyMap() {
233
+ if (this.resizeObserver) {
234
+ this.resizeObserver.disconnect();
235
+ this.resizeObserver = undefined;
236
+ }
203
237
  if (this.map) {
204
238
  this.zoneHelper.runOutsideAngular(() => {
205
239
  this.map.setTarget(undefined);
@@ -254,16 +288,148 @@ class OlMapComponent {
254
288
  });
255
289
  }
256
290
  }
257
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
258
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.4", type: OlMapComponent, isStandalone: true, selector: "ol-map", inputs: { center: { classPropertyName: "center", publicName: "center", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, rotation: { classPropertyName: "rotation", publicName: "rotation", isSignal: true, isRequired: false, transformFunction: null }, projection: { classPropertyName: "projection", publicName: "projection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewChange: "viewChange", mapClick: "mapClick", mapDblClick: "mapDblClick" }, viewQueries: [{ propertyName: "mapContainerRef", first: true, predicate: ["mapContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `<div class="ol-map-container" #mapContainer></div>
291
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
292
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.13", type: OlMapComponent, isStandalone: true, selector: "ol-map", inputs: { center: { classPropertyName: "center", publicName: "center", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, rotation: { classPropertyName: "rotation", publicName: "rotation", isSignal: true, isRequired: false, transformFunction: null }, projection: { classPropertyName: "projection", publicName: "projection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewChange: "viewChange", mapClick: "mapClick", mapDblClick: "mapDblClick" }, viewQueries: [{ propertyName: "mapContainerRef", first: true, predicate: ["mapContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `<div class="ol-map-container" #mapContainer></div>
259
293
  <ng-content />`, isInline: true, styles: [":host{display:block;width:100%;height:100%;position:relative}.ol-map-container{width:100%;height:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
260
294
  }
261
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlMapComponent, decorators: [{
295
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapComponent, decorators: [{
262
296
  type: Component,
263
297
  args: [{ selector: 'ol-map', template: `<div class="ol-map-container" #mapContainer></div>
264
298
  <ng-content />`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%;height:100%;position:relative}.ol-map-container{width:100%;height:100%}\n"] }]
265
299
  }], ctorParameters: () => [], propDecorators: { center: [{ type: i0.Input, args: [{ isSignal: true, alias: "center", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], rotation: [{ type: i0.Input, args: [{ isSignal: true, alias: "rotation", required: false }] }], projection: [{ type: i0.Input, args: [{ isSignal: true, alias: "projection", required: false }] }], viewChange: [{ type: i0.Output, args: ["viewChange"] }], mapClick: [{ type: i0.Output, args: ["mapClick"] }], mapDblClick: [{ type: i0.Output, args: ["mapDblClick"] }], mapContainerRef: [{ type: i0.ViewChild, args: ['mapContainer', { isSignal: true }] }] } });
266
300
 
301
+ // OlGeometryService — general purpose geometry helpers
302
+ /**
303
+ * Service exposing general purpose geometry helpers for creating
304
+ * approximated polygons (ellipses, sectors, donuts) from metric parameters.
305
+ */
306
+ class OlGeometryService {
307
+ idCounter = 0;
308
+ /**
309
+ * Build a `Feature<Polygon>` approximating an ellipse centered at
310
+ * `config.center`. See {@link EllipseConfig} for parameter semantics.
311
+ */
312
+ createEllipse(config) {
313
+ const { center, semiMajor, semiMinor, rotation = 0, segments = 64, properties } = config;
314
+ if (semiMajor <= 0 || semiMinor <= 0) {
315
+ throw new RangeError('semiMajor and semiMinor must be positive');
316
+ }
317
+ if (segments < 8) {
318
+ throw new RangeError('segments must be >= 8');
319
+ }
320
+ const cosR = Math.cos(rotation);
321
+ const sinR = Math.sin(rotation);
322
+ const ring = [];
323
+ for (let i = 0; i < segments; i++) {
324
+ const theta = (i / segments) * Math.PI * 2;
325
+ // Ellipse in local axis-aligned frame, then rotated by `rotation`.
326
+ const ax = Math.cos(theta) * semiMajor;
327
+ const ay = Math.sin(theta) * semiMinor;
328
+ const dx = ax * cosR - ay * sinR;
329
+ const dy = ax * sinR + ay * cosR;
330
+ ring.push(this.offsetMetersToLonLat(center, dx, dy));
331
+ }
332
+ ring.push(ring[0]); // close the ring
333
+ return {
334
+ id: this.nextId('ellipse'),
335
+ geometry: { type: 'Polygon', coordinates: [ring] },
336
+ properties,
337
+ };
338
+ }
339
+ /**
340
+ * Build a `Feature<Polygon>` for a circular sector (pie slice).
341
+ * See {@link SectorConfig} for parameter semantics.
342
+ */
343
+ createSector(config) {
344
+ const { center, radius, startAngle, endAngle, segments = 32, properties } = config;
345
+ if (radius <= 0) {
346
+ throw new RangeError('radius must be positive');
347
+ }
348
+ if (endAngle <= startAngle) {
349
+ throw new RangeError('endAngle must be greater than startAngle');
350
+ }
351
+ if (endAngle - startAngle > Math.PI * 2) {
352
+ throw new RangeError('sector cannot exceed full circle');
353
+ }
354
+ if (segments < 4) {
355
+ throw new RangeError('segments must be >= 4');
356
+ }
357
+ const ring = [center];
358
+ const span = endAngle - startAngle;
359
+ for (let i = 0; i <= segments; i++) {
360
+ const theta = startAngle + (i / segments) * span;
361
+ const dx = Math.cos(theta) * radius;
362
+ const dy = Math.sin(theta) * radius;
363
+ ring.push(this.offsetMetersToLonLat(center, dx, dy));
364
+ }
365
+ ring.push(center); // close back to apex
366
+ return {
367
+ id: this.nextId('sector'),
368
+ geometry: { type: 'Polygon', coordinates: [ring] },
369
+ properties,
370
+ };
371
+ }
372
+ /**
373
+ * Build a `Feature<Polygon>` for a donut (annular ring). The output has
374
+ * two rings: an outer ring wound counter-clockwise and an inner ring
375
+ * wound clockwise so the GeoJSON right-hand rule renders the hole.
376
+ */
377
+ createDonut(config) {
378
+ const { center, outerRadius, innerRadius, segments = 64, properties } = config;
379
+ if (outerRadius <= 0 || innerRadius <= 0) {
380
+ throw new RangeError('radii must be positive');
381
+ }
382
+ if (outerRadius <= innerRadius) {
383
+ throw new RangeError('outerRadius must be greater than innerRadius');
384
+ }
385
+ if (segments < 8) {
386
+ throw new RangeError('segments must be >= 8');
387
+ }
388
+ const outer = [];
389
+ const inner = [];
390
+ for (let i = 0; i < segments; i++) {
391
+ const theta = (i / segments) * Math.PI * 2;
392
+ const cosT = Math.cos(theta);
393
+ const sinT = Math.sin(theta);
394
+ // Outer ring: CCW (theta increasing)
395
+ outer.push(this.offsetMetersToLonLat(center, cosT * outerRadius, sinT * outerRadius));
396
+ // Inner ring: CW — sample the SAME thetas but we'll reverse the
397
+ // accumulator below so the ring is traversed in the opposite sense.
398
+ inner.push(this.offsetMetersToLonLat(center, cosT * innerRadius, sinT * innerRadius));
399
+ }
400
+ inner.reverse();
401
+ outer.push(outer[0]);
402
+ inner.push(inner[0]);
403
+ return {
404
+ id: this.nextId('donut'),
405
+ geometry: { type: 'Polygon', coordinates: [outer, inner] },
406
+ properties,
407
+ };
408
+ }
409
+ /**
410
+ * Project an `(dx, dy)` meter offset from `center` to lon/lat using true
411
+ * geodesic math (Vincenty's formulae) via ol/sphere.
412
+ */
413
+ offsetMetersToLonLat(center, dx, dy) {
414
+ if (dx === 0 && dy === 0)
415
+ return [...center];
416
+ const distance = Math.hypot(dx, dy);
417
+ const bearing = Math.atan2(dx, dy);
418
+ return offset(center, distance, bearing);
419
+ }
420
+ nextId(kind) {
421
+ return `${kind}-${++this.idCounter}`;
422
+ }
423
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlGeometryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
424
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlGeometryService, providedIn: 'root' });
425
+ }
426
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlGeometryService, decorators: [{
427
+ type: Injectable,
428
+ args: [{
429
+ providedIn: 'root',
430
+ }]
431
+ }] });
432
+
267
433
  // Provider functions
268
434
  function provideOpenLayers(...features) {
269
435
  return makeEnvironmentProviders([
@@ -273,10 +439,43 @@ function provideOpenLayers(...features) {
273
439
  ]);
274
440
  }
275
441
 
442
+ /**
443
+ * Registers custom projections using proj4.
444
+ *
445
+ * @param proj4 - The proj4 instance (must be passed to avoid strong dependency on proj4 package)
446
+ * @param definitions - Array of projection definitions
447
+ * @returns OlFeature for projections
448
+ */
449
+ function withProjections(proj4, definitions) {
450
+ return {
451
+ kind: 'projections',
452
+ providers: [
453
+ {
454
+ provide: ENVIRONMENT_INITIALIZER,
455
+ multi: true,
456
+ useValue: () => {
457
+ definitions.forEach((d) => {
458
+ proj4.defs(d.code, d.def);
459
+ });
460
+ register(proj4);
461
+ definitions.forEach((d) => {
462
+ if (d.extent) {
463
+ const proj = get(d.code);
464
+ if (proj) {
465
+ proj.setExtent(d.extent);
466
+ }
467
+ }
468
+ });
469
+ },
470
+ },
471
+ ],
472
+ };
473
+ }
474
+
276
475
  // @angular-helpers/openlayers/core
277
476
 
278
477
  /**
279
478
  * Generated bundle index. Do not edit.
280
479
  */
281
480
 
282
- export { OlMapComponent, OlMapService, OlZoneHelper, provideOpenLayers };
481
+ export { OlGeometryService, OlMapComponent, OlMapService, OlZoneHelper, provideOpenLayers, withProjections };