@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,8 +1,13 @@
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, computed, resource, 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 GeoJSON from 'ol/format/GeoJSON';
8
+ import FeatureClass from 'ol/Feature';
9
+ import { Point, LineString, Polygon } from 'ol/geom';
10
+ import { register } from 'ol/proj/proj4';
6
11
 
7
12
  // ZoneHelperService - Handles NgZone compatibility for zoneless mode
8
13
  /**
@@ -52,10 +57,10 @@ class OlZoneHelper {
52
57
  }
53
58
  return fn();
54
59
  }
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 });
60
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlZoneHelper, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
61
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlZoneHelper });
57
62
  }
58
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlZoneHelper, decorators: [{
63
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlZoneHelper, decorators: [{
59
64
  type: Injectable
60
65
  }] });
61
66
 
@@ -64,13 +69,22 @@ class OlMapService {
64
69
  zoneHelper = inject(OlZoneHelper);
65
70
  map = null;
66
71
  readyCallbacks = [];
72
+ _resolution = signal(1, ...(ngDevMode ? [{ debugName: "_resolution" }] : /* istanbul ignore next */ []));
73
+ /** Signal that emits the current map resolution in meters per pixel */
74
+ resolution = this._resolution.asReadonly();
67
75
  setMap(map) {
68
76
  this.map = map;
69
- const callbacks = this.readyCallbacks.splice(0);
70
- for (const cb of callbacks) {
71
- cb(map);
77
+ if (map) {
78
+ this._resolution.set(map.getView()?.getResolution() ?? 1);
79
+ const callbacks = this.readyCallbacks.splice(0);
80
+ for (const cb of callbacks) {
81
+ cb(map);
82
+ }
72
83
  }
73
84
  }
85
+ setResolution(resolution) {
86
+ this._resolution.set(resolution);
87
+ }
74
88
  getMap() {
75
89
  return this.map;
76
90
  }
@@ -134,10 +148,10 @@ class OlMapService {
134
148
  rotation: view.getRotation() ?? 0,
135
149
  };
136
150
  }
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 });
151
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
152
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapService });
139
153
  }
140
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlMapService, decorators: [{
154
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapService, decorators: [{
141
155
  type: Injectable
142
156
  }] });
143
157
 
@@ -201,7 +215,12 @@ class OlMapComponent {
201
215
  this.resizeObserver.observe(container);
202
216
  }
203
217
  view.on('change:center', () => this.zoneHelper.runInsideAngular(() => this.emitViewChange()));
204
- view.on('change:resolution', () => this.zoneHelper.runInsideAngular(() => this.emitViewChange()));
218
+ view.on('change:resolution', () => {
219
+ this.zoneHelper.runInsideAngular(() => {
220
+ this.mapService.setResolution(view.getResolution() ?? 1);
221
+ this.emitViewChange();
222
+ });
223
+ });
205
224
  this.map.on('click', (e) => this.zoneHelper.runInsideAngular(() => this.mapClick.emit({
206
225
  coordinate: toLonLat(e.coordinate, this.projection()),
207
226
  pixel: e.pixel,
@@ -272,16 +291,358 @@ class OlMapComponent {
272
291
  });
273
292
  }
274
293
  }
275
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
276
- 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>
294
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
295
+ 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>
277
296
  <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 });
278
297
  }
279
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlMapComponent, decorators: [{
298
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapComponent, decorators: [{
280
299
  type: Component,
281
300
  args: [{ selector: 'ol-map', template: `<div class="ol-map-container" #mapContainer></div>
282
301
  <ng-content />`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%;height:100%;position:relative}.ol-map-container{width:100%;height:100%}\n"] }]
283
302
  }], 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 }] }] } });
284
303
 
304
+ // OlGeometryService — general purpose geometry helpers
305
+ /**
306
+ * Service exposing general purpose geometry helpers for creating
307
+ * approximated polygons (ellipses, sectors, donuts) from metric parameters.
308
+ */
309
+ class OlGeometryService {
310
+ idCounter = 0;
311
+ /**
312
+ * Build a `Feature<Polygon>` approximating an ellipse centered at
313
+ * `config.center`. See {@link EllipseConfig} for parameter semantics.
314
+ */
315
+ createEllipse(config) {
316
+ const { center, semiMajor, semiMinor, rotation = 0, segments = 64, properties } = config;
317
+ if (semiMajor <= 0 || semiMinor <= 0) {
318
+ throw new RangeError('semiMajor and semiMinor must be positive');
319
+ }
320
+ if (segments < 8) {
321
+ throw new RangeError('segments must be >= 8');
322
+ }
323
+ const cosR = Math.cos(rotation);
324
+ const sinR = Math.sin(rotation);
325
+ const ring = [];
326
+ for (let i = 0; i < segments; i++) {
327
+ const theta = (i / segments) * Math.PI * 2;
328
+ // Ellipse in local axis-aligned frame, then rotated by `rotation`.
329
+ const ax = Math.cos(theta) * semiMajor;
330
+ const ay = Math.sin(theta) * semiMinor;
331
+ const dx = ax * cosR - ay * sinR;
332
+ const dy = ax * sinR + ay * cosR;
333
+ ring.push(this.offsetMetersToLonLat(center, dx, dy));
334
+ }
335
+ ring.push(ring[0]); // close the ring
336
+ return {
337
+ id: this.nextId('ellipse'),
338
+ geometry: { type: 'Polygon', coordinates: [ring] },
339
+ properties,
340
+ };
341
+ }
342
+ /**
343
+ * Build a `Feature<Polygon>` for a circular sector (pie slice).
344
+ * See {@link SectorConfig} for parameter semantics.
345
+ */
346
+ createSector(config) {
347
+ const { center, radius, startAngle, endAngle, segments = 32, properties } = config;
348
+ if (radius <= 0) {
349
+ throw new RangeError('radius must be positive');
350
+ }
351
+ if (endAngle <= startAngle) {
352
+ throw new RangeError('endAngle must be greater than startAngle');
353
+ }
354
+ if (endAngle - startAngle > Math.PI * 2) {
355
+ throw new RangeError('sector cannot exceed full circle');
356
+ }
357
+ if (segments < 4) {
358
+ throw new RangeError('segments must be >= 4');
359
+ }
360
+ const ring = [center];
361
+ const span = endAngle - startAngle;
362
+ for (let i = 0; i <= segments; i++) {
363
+ const theta = startAngle + (i / segments) * span;
364
+ const dx = Math.cos(theta) * radius;
365
+ const dy = Math.sin(theta) * radius;
366
+ ring.push(this.offsetMetersToLonLat(center, dx, dy));
367
+ }
368
+ ring.push(center); // close back to apex
369
+ return {
370
+ id: this.nextId('sector'),
371
+ geometry: { type: 'Polygon', coordinates: [ring] },
372
+ properties,
373
+ };
374
+ }
375
+ /**
376
+ * Build a `Feature<Polygon>` for a donut (annular ring). The output has
377
+ * two rings: an outer ring wound counter-clockwise and an inner ring
378
+ * wound clockwise so the GeoJSON right-hand rule renders the hole.
379
+ */
380
+ createDonut(config) {
381
+ const { center, outerRadius, innerRadius, segments = 64, properties } = config;
382
+ if (outerRadius <= 0 || innerRadius <= 0) {
383
+ throw new RangeError('radii must be positive');
384
+ }
385
+ if (outerRadius <= innerRadius) {
386
+ throw new RangeError('outerRadius must be greater than innerRadius');
387
+ }
388
+ if (segments < 8) {
389
+ throw new RangeError('segments must be >= 8');
390
+ }
391
+ const outer = [];
392
+ const inner = [];
393
+ for (let i = 0; i < segments; i++) {
394
+ const theta = (i / segments) * Math.PI * 2;
395
+ const cosT = Math.cos(theta);
396
+ const sinT = Math.sin(theta);
397
+ // Outer ring: CCW (theta increasing)
398
+ outer.push(this.offsetMetersToLonLat(center, cosT * outerRadius, sinT * outerRadius));
399
+ // Inner ring: CW — sample the SAME thetas but we'll reverse the
400
+ // accumulator below so the ring is traversed in the opposite sense.
401
+ inner.push(this.offsetMetersToLonLat(center, cosT * innerRadius, sinT * innerRadius));
402
+ }
403
+ inner.reverse();
404
+ outer.push(outer[0]);
405
+ inner.push(inner[0]);
406
+ return {
407
+ id: this.nextId('donut'),
408
+ geometry: { type: 'Polygon', coordinates: [outer, inner] },
409
+ properties,
410
+ };
411
+ }
412
+ /**
413
+ * Project an `(dx, dy)` meter offset from `center` to lon/lat using true
414
+ * geodesic math (Vincenty's formulae) via ol/sphere.
415
+ */
416
+ offsetMetersToLonLat(center, dx, dy) {
417
+ if (dx === 0 && dy === 0)
418
+ return [...center];
419
+ const distance = Math.hypot(dx, dy);
420
+ const bearing = Math.atan2(dx, dy);
421
+ return offset(center, distance, bearing);
422
+ }
423
+ nextId(kind) {
424
+ return `${kind}-${++this.idCounter}`;
425
+ }
426
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlGeometryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
427
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlGeometryService, providedIn: 'root' });
428
+ }
429
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlGeometryService, decorators: [{
430
+ type: Injectable,
431
+ args: [{
432
+ providedIn: 'root',
433
+ }]
434
+ }] });
435
+
436
+ /**
437
+ * Service for orchestrating time-series animations in OpenLayers.
438
+ * Exposes a reactive currentTime signal that updates outside the Angular zone
439
+ * via requestAnimationFrame, ensuring 60FPS WebGL animations without triggering
440
+ * global change detection.
441
+ */
442
+ class OlTimeService {
443
+ zoneHelper = inject(OlZoneHelper);
444
+ timeSignal = signal(Date.now(), ...(ngDevMode ? [{ debugName: "timeSignal" }] : /* istanbul ignore next */ []));
445
+ playingSignal = signal(false, ...(ngDevMode ? [{ debugName: "playingSignal" }] : /* istanbul ignore next */ []));
446
+ speedSignal = signal(1, ...(ngDevMode ? [{ debugName: "speedSignal" }] : /* istanbul ignore next */ []));
447
+ animationFrameId = null;
448
+ lastTick = 0;
449
+ currentTime = computed(() => this.timeSignal(), ...(ngDevMode ? [{ debugName: "currentTime" }] : /* istanbul ignore next */ []));
450
+ isPlaying = computed(() => this.playingSignal(), ...(ngDevMode ? [{ debugName: "isPlaying" }] : /* istanbul ignore next */ []));
451
+ speed = computed(() => this.speedSignal(), ...(ngDevMode ? [{ debugName: "speed" }] : /* istanbul ignore next */ []));
452
+ /**
453
+ * Sets the current time manually.
454
+ * @param time Epoch timestamp in milliseconds
455
+ */
456
+ setTime(time) {
457
+ this.timeSignal.set(time);
458
+ }
459
+ /**
460
+ * Sets the playback speed multiplier.
461
+ * @param speed Multiplier (e.g. 1 = real time, 60 = 1 minute per second)
462
+ */
463
+ setSpeed(speed) {
464
+ this.speedSignal.set(speed);
465
+ }
466
+ /**
467
+ * Starts the time animation loop.
468
+ */
469
+ play() {
470
+ if (this.playingSignal())
471
+ return;
472
+ this.playingSignal.set(true);
473
+ this.lastTick = performance.now();
474
+ this.zoneHelper.runOutsideAngular(() => {
475
+ const loop = (now) => {
476
+ if (!this.playingSignal())
477
+ return;
478
+ const delta = now - this.lastTick;
479
+ this.lastTick = now;
480
+ // Advance time based on delta and speed multiplier
481
+ const advance = delta * this.speedSignal();
482
+ this.timeSignal.update((t) => t + advance);
483
+ this.animationFrameId = requestAnimationFrame(loop);
484
+ };
485
+ this.animationFrameId = requestAnimationFrame(loop);
486
+ });
487
+ }
488
+ /**
489
+ * Pauses the time animation loop.
490
+ */
491
+ pause() {
492
+ this.playingSignal.set(false);
493
+ if (this.animationFrameId !== null) {
494
+ cancelAnimationFrame(this.animationFrameId);
495
+ this.animationFrameId = null;
496
+ }
497
+ }
498
+ /**
499
+ * Stops the animation and resets to a specific time.
500
+ */
501
+ stop(resetTime = Date.now()) {
502
+ this.pause();
503
+ this.setTime(resetTime);
504
+ }
505
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
506
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimeService, providedIn: 'root' });
507
+ }
508
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimeService, decorators: [{
509
+ type: Injectable,
510
+ args: [{ providedIn: 'root' }]
511
+ }] });
512
+
513
+ // Feature conversion utilities for OpenLayers interactions
514
+ /**
515
+ * Converts an OpenLayers feature to the internal Feature format.
516
+ * Handles coordinate extraction and geometry type mapping.
517
+ *
518
+ * @param olFeature - The OpenLayers feature to convert
519
+ * @returns The converted Feature with normalized structure
520
+ */
521
+ function olFeatureToFeature(olFeature) {
522
+ // Unwrap spider features
523
+ const spiderFeature = olFeature.get('spider-feature');
524
+ if (spiderFeature) {
525
+ return olFeatureToFeature(spiderFeature);
526
+ }
527
+ // Unwrap single-item clusters
528
+ const clusterFeatures = olFeature.get('features');
529
+ if (Array.isArray(clusterFeatures) && clusterFeatures.length === 1) {
530
+ return olFeatureToFeature(clusterFeatures[0]);
531
+ }
532
+ const geometry = olFeature.getGeometry();
533
+ const geomType = geometry?.getType() ?? 'Point';
534
+ // Convert coordinates based on geometry type
535
+ let coordinates;
536
+ if (geomType === 'Circle') {
537
+ // ol/geom/Circle has no getCoordinates() — use getCenter() instead
538
+ const circle = geometry;
539
+ coordinates = circle.getCenter();
540
+ }
541
+ else {
542
+ // oxlint-disable-next-line no-explicit-any
543
+ const olCoords = geometry.getCoordinates();
544
+ if (Array.isArray(olCoords) && Array.isArray(olCoords[0])) {
545
+ // Multi-coordinate structures (LineString, Polygon, etc.)
546
+ coordinates = olCoords;
547
+ }
548
+ else {
549
+ // Single point
550
+ coordinates = olCoords;
551
+ }
552
+ }
553
+ return {
554
+ id: olFeature.getId()?.toString() ?? `feature-${Math.random().toString(36).slice(2)}`,
555
+ geometry: {
556
+ type: geomType,
557
+ coordinates,
558
+ },
559
+ properties: olFeature.getProperties(),
560
+ };
561
+ }
562
+ /**
563
+ * Converts an internal Feature to an OpenLayers feature.
564
+ */
565
+ function featureToOlFeature(feature) {
566
+ const geom = feature.geometry;
567
+ let geometry;
568
+ if (!geom.coordinates) {
569
+ geometry = new Point([0, 0]);
570
+ }
571
+ else if (geom.type === 'Point') {
572
+ const coords = geom.coordinates;
573
+ geometry = new Point(fromLonLat(coords));
574
+ }
575
+ else if (geom.type === 'LineString') {
576
+ const coords = geom.coordinates.map((c) => fromLonLat(c));
577
+ geometry = new LineString(coords);
578
+ }
579
+ else if (geom.type === 'Polygon') {
580
+ const rings = geom.coordinates.map((ring) => ring.map((c) => fromLonLat(c)));
581
+ geometry = new Polygon(rings);
582
+ }
583
+ else {
584
+ geometry = new Point([0, 0]);
585
+ }
586
+ // Create OL Feature
587
+ // Note: we must avoid passing 'geometry' as a plain object to OLFeature constructor,
588
+ // so we pass the object properties without it, then set geometry explicitly.
589
+ const props = { ...feature.properties };
590
+ delete props['geometry']; // Just in case
591
+ const olFeature = new FeatureClass(props);
592
+ olFeature.setGeometry(geometry);
593
+ olFeature.setId(feature.id);
594
+ return olFeature;
595
+ }
596
+
597
+ /**
598
+ * Creates an Angular resource for fetching and decoding GeoJSON into OpenLayers Features.
599
+ * Must be called in an injection context.
600
+ *
601
+ * @param url The URL signal or string to fetch data from
602
+ * @param options Additional vector resource options
603
+ * @returns An Angular Resource containing an array of parsed Features
604
+ */
605
+ function createVectorResource(url, options) {
606
+ return resource({
607
+ loader: async ({ abortSignal }) => {
608
+ const fetchUrl = url();
609
+ if (!fetchUrl)
610
+ return [];
611
+ const cacheKey = 'ol-vector-cache-v1';
612
+ const isBrowser = typeof caches !== 'undefined';
613
+ let response;
614
+ let cache;
615
+ if (isBrowser) {
616
+ cache = await caches.open(cacheKey);
617
+ const cachedResponse = await cache.match(fetchUrl);
618
+ if (cachedResponse) {
619
+ response = cachedResponse;
620
+ }
621
+ }
622
+ if (!response) {
623
+ response = await fetch(fetchUrl, {
624
+ ...options?.fetchOptions,
625
+ signal: abortSignal,
626
+ });
627
+ if (!response.ok) {
628
+ throw new Error(`Failed to fetch vector data: ${response.statusText}`);
629
+ }
630
+ if (isBrowser && cache) {
631
+ await cache.put(fetchUrl, response.clone());
632
+ }
633
+ }
634
+ // We process the text since GeoJSON readFeatures accepts string or object.
635
+ // Doing text() might be slightly faster before passing to OL's parser.
636
+ const data = await response.text();
637
+ const format = new GeoJSON();
638
+ // We parse the GeoJSON string into OpenLayers features.
639
+ const olFeatures = format.readFeatures(data);
640
+ // Convert to our generic Feature interface expected by OlVectorLayer
641
+ return olFeatures.map((f) => olFeatureToFeature(f));
642
+ },
643
+ });
644
+ }
645
+
285
646
  // Provider functions
286
647
  function provideOpenLayers(...features) {
287
648
  return makeEnvironmentProviders([
@@ -291,10 +652,43 @@ function provideOpenLayers(...features) {
291
652
  ]);
292
653
  }
293
654
 
655
+ /**
656
+ * Registers custom projections using proj4.
657
+ *
658
+ * @param proj4 - The proj4 instance (must be passed to avoid strong dependency on proj4 package)
659
+ * @param definitions - Array of projection definitions
660
+ * @returns OlFeature for projections
661
+ */
662
+ function withProjections(proj4, definitions) {
663
+ return {
664
+ kind: 'projections',
665
+ providers: [
666
+ {
667
+ provide: ENVIRONMENT_INITIALIZER,
668
+ multi: true,
669
+ useValue: () => {
670
+ definitions.forEach((d) => {
671
+ proj4.defs(d.code, d.def);
672
+ });
673
+ register(proj4);
674
+ definitions.forEach((d) => {
675
+ if (d.extent) {
676
+ const proj = get(d.code);
677
+ if (proj) {
678
+ proj.setExtent(d.extent);
679
+ }
680
+ }
681
+ });
682
+ },
683
+ },
684
+ ],
685
+ };
686
+ }
687
+
294
688
  // @angular-helpers/openlayers/core
295
689
 
296
690
  /**
297
691
  * Generated bundle index. Do not edit.
298
692
  */
299
693
 
300
- export { OlMapComponent, OlMapService, OlZoneHelper, provideOpenLayers };
694
+ export { OlGeometryService, OlMapComponent, OlMapService, OlTimeService, OlZoneHelper, createVectorResource, featureToOlFeature, olFeatureToFeature, provideOpenLayers, withProjections };