@angular-helpers/openlayers 0.5.1 → 0.6.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.
package/README.md CHANGED
@@ -45,7 +45,7 @@ export const appConfig: ApplicationConfig = {
45
45
 
46
46
  ### 2. Use in your component
47
47
 
48
- ```typescript
48
+ ````typescript
49
49
  // map.component.ts
50
50
  import { Component, inject, signal } from '@angular/core';
51
51
  import { OlMapComponent } from '@angular-helpers/openlayers/core';
@@ -103,8 +103,57 @@ export class MapComponent {
103
103
  this.mapService.animateView({ center: [2.2945, 48.8584], zoom: 15, duration: 1000 });
104
104
  }
105
105
  }
106
+ ## Custom Projections & Coordinate Systems
107
+
108
+ Available since `0.4.1` in `@angular-helpers/openlayers/core` and `@angular-helpers/openlayers/layers`.
109
+
110
+ When working with local reference systems (like UTM zones), you can register custom projections globally and pass coordinates in those reference systems directly to `[center]` or `[features]` without manual transforms:
111
+
112
+ ### 1. Register custom projections globally
113
+
114
+ ```typescript
115
+ // app.config.ts
116
+ import { provideOpenLayers } from '@angular-helpers/openlayers/core';
117
+ import { withProjections } from '@angular-helpers/openlayers/core';
118
+ import proj4 from 'proj4';
119
+
120
+ export const appConfig = {
121
+ providers: [
122
+ provideOpenLayers(
123
+ withProjections(proj4, [
124
+ {
125
+ code: 'EPSG:25830', // UTM Zone 30N
126
+ def: '+proj=utm +zone=30 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs',
127
+ extent: [0, 0, 1000000, 10000000],
128
+ },
129
+ ]),
130
+ ),
131
+ ],
132
+ };
133
+ ````
134
+
135
+ ### 2. Pass local coordinates natively to the map
136
+
137
+ By setting `[coordinateProjection]` to match the custom projection, the component automatically bypasses longitude/latitude conversion, feeding UTM coordinates directly into OpenLayers:
138
+
139
+ ```html
140
+ <ol-map
141
+ [projection]="'EPSG:25830'"
142
+ [coordinateProjection]="'EPSG:25830'"
143
+ [center]="[440291, 4474255]" <!-- Madrid UTM Zone 30 coordinates -->
144
+ [zoom]="12"
145
+ (viewChange)="onViewChange($event)"
146
+ >
147
+ <ol-vector-layer
148
+ id="shapes"
149
+ [features]="utmFeatures()"
150
+ [coordinateProjection]="'EPSG:25830'"
151
+ />
152
+ </ol-map>
106
153
  ```
107
154
 
155
+ ---
156
+
108
157
  ## Overlays — popups and tooltips
109
158
 
110
159
  Available since `0.3.0` from `@angular-helpers/openlayers/overlays`.
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
2
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, get } from 'ol/proj';
5
+ import { transform, get } from 'ol/proj';
6
6
  import { offset } from 'ol/sphere';
7
7
  import GeoJSON from 'ol/format/GeoJSON';
8
8
  import FeatureClass from 'ol/Feature';
@@ -164,6 +164,7 @@ class OlMapComponent {
164
164
  zoom = input(0, ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
165
165
  rotation = input(0, ...(ngDevMode ? [{ debugName: "rotation" }] : /* istanbul ignore next */ []));
166
166
  projection = input('EPSG:3857', ...(ngDevMode ? [{ debugName: "projection" }] : /* istanbul ignore next */ []));
167
+ coordinateProjection = input('EPSG:4326', ...(ngDevMode ? [{ debugName: "coordinateProjection" }] : /* istanbul ignore next */ [])); // Dynamic input for coordinate systems
167
168
  viewChange = output();
168
169
  mapClick = output();
169
170
  mapDblClick = output();
@@ -190,11 +191,25 @@ class OlMapComponent {
190
191
  // Cleanup when component is destroyed
191
192
  this.destroyRef.onDestroy(() => this.destroyMap());
192
193
  }
194
+ getProjectedCoordinate(coord) {
195
+ const coordProj = this.coordinateProjection();
196
+ const mapProj = this.projection();
197
+ if (coordProj === mapProj)
198
+ return coord;
199
+ return transform(coord, coordProj, mapProj);
200
+ }
201
+ getExternalCoordinate(coord) {
202
+ const coordProj = this.coordinateProjection();
203
+ const mapProj = this.projection();
204
+ if (coordProj === mapProj)
205
+ return coord;
206
+ return transform(coord, mapProj, coordProj);
207
+ }
193
208
  initMap() {
194
209
  const container = this.mapContainerRef().nativeElement;
195
210
  this.zoneHelper.runOutsideAngular(() => {
196
211
  const view = new View({
197
- center: fromLonLat(this.center(), this.projection()),
212
+ center: this.getProjectedCoordinate(this.center()),
198
213
  zoom: this.zoom(),
199
214
  rotation: this.rotation(),
200
215
  projection: this.projection(),
@@ -222,11 +237,11 @@ class OlMapComponent {
222
237
  });
223
238
  });
224
239
  this.map.on('click', (e) => this.zoneHelper.runInsideAngular(() => this.mapClick.emit({
225
- coordinate: toLonLat(e.coordinate, this.projection()),
240
+ coordinate: this.getExternalCoordinate(e.coordinate),
226
241
  pixel: e.pixel,
227
242
  })));
228
243
  this.map.on('dblclick', (e) => this.zoneHelper.runInsideAngular(() => this.mapDblClick.emit({
229
- coordinate: toLonLat(e.coordinate, this.projection()),
244
+ coordinate: this.getExternalCoordinate(e.coordinate),
230
245
  pixel: e.pixel,
231
246
  })));
232
247
  });
@@ -250,7 +265,7 @@ class OlMapComponent {
250
265
  if (!this.map)
251
266
  return;
252
267
  const view = this.map.getView();
253
- const projectedCenter = fromLonLat(center, this.projection());
268
+ const projectedCenter = this.getProjectedCoordinate(center);
254
269
  const currentCenter = view.getCenter();
255
270
  // Only update if center is significantly different (prevents interfering with animations)
256
271
  if (!currentCenter ||
@@ -283,23 +298,23 @@ class OlMapComponent {
283
298
  const view = this.map?.getView();
284
299
  if (view) {
285
300
  const projectedCenter = view.getCenter() ?? [0, 0];
286
- const lonLatCenter = toLonLat(projectedCenter, this.projection());
301
+ const externalCenter = this.getExternalCoordinate(projectedCenter);
287
302
  this.viewChange.emit({
288
- center: lonLatCenter,
303
+ center: externalCenter,
289
304
  zoom: view.getZoom() ?? 0,
290
305
  rotation: view.getRotation() ?? 0,
291
306
  });
292
307
  }
293
308
  }
294
309
  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>
310
+ 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 }, coordinateProjection: { classPropertyName: "coordinateProjection", publicName: "coordinateProjection", 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>
296
311
  <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 });
297
312
  }
298
313
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapComponent, decorators: [{
299
314
  type: Component,
300
315
  args: [{ selector: 'ol-map', template: `<div class="ol-map-container" #mapContainer></div>
301
316
  <ng-content />`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%;height:100%;position:relative}.ol-map-container{width:100%;height:100%}\n"] }]
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 }] }] } });
317
+ }], 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 }] }], coordinateProjection: [{ type: i0.Input, args: [{ isSignal: true, alias: "coordinateProjection", 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 }] }] } });
303
318
 
304
319
  // OlGeometryService — general purpose geometry helpers
305
320
  /**
@@ -511,44 +526,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
511
526
  }] });
512
527
 
513
528
  // Feature conversion utilities for OpenLayers interactions
529
+ function transformCoords(coords, sourceProj, targetProj) {
530
+ if (!sourceProj || !targetProj || sourceProj === targetProj)
531
+ return coords;
532
+ if (!Array.isArray(coords))
533
+ return coords;
534
+ if (typeof coords[0] === 'number') {
535
+ return transform(coords, sourceProj, targetProj);
536
+ }
537
+ return coords.map((c) => transformCoords(c, sourceProj, targetProj));
538
+ }
514
539
  /**
515
540
  * Converts an OpenLayers feature to the internal Feature format.
516
- * Handles coordinate extraction and geometry type mapping.
541
+ * Handles coordinate extraction and geometry type mapping with custom projections.
517
542
  *
518
543
  * @param olFeature - The OpenLayers feature to convert
544
+ * @param options - Projection source and target codes
519
545
  * @returns The converted Feature with normalized structure
520
546
  */
521
- function olFeatureToFeature(olFeature) {
547
+ function olFeatureToFeature(olFeature, options) {
522
548
  // Unwrap spider features
523
549
  const spiderFeature = olFeature.get('spider-feature');
524
550
  if (spiderFeature) {
525
- return olFeatureToFeature(spiderFeature);
551
+ return olFeatureToFeature(spiderFeature, options);
526
552
  }
527
553
  // Unwrap single-item clusters
528
554
  const clusterFeatures = olFeature.get('features');
529
555
  if (Array.isArray(clusterFeatures) && clusterFeatures.length === 1) {
530
- return olFeatureToFeature(clusterFeatures[0]);
556
+ return olFeatureToFeature(clusterFeatures[0], options);
531
557
  }
532
558
  const geometry = olFeature.getGeometry();
533
559
  const geomType = geometry?.getType() ?? 'Point';
560
+ const sourceProj = options?.targetProjection;
561
+ const targetProj = options?.sourceProjection;
534
562
  // Convert coordinates based on geometry type
535
563
  let coordinates;
536
564
  if (geomType === 'Circle') {
537
565
  // ol/geom/Circle has no getCoordinates() — use getCenter() instead
538
566
  const circle = geometry;
539
- coordinates = circle.getCenter();
567
+ coordinates = transformCoords(circle.getCenter(), sourceProj, targetProj);
540
568
  }
541
569
  else {
542
570
  // oxlint-disable-next-line no-explicit-any
543
571
  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
- }
572
+ coordinates = transformCoords(olCoords, sourceProj, targetProj);
552
573
  }
553
574
  return {
554
575
  id: olFeature.getId()?.toString() ?? `feature-${Math.random().toString(36).slice(2)}`,
@@ -562,7 +583,9 @@ function olFeatureToFeature(olFeature) {
562
583
  /**
563
584
  * Converts an internal Feature to an OpenLayers feature.
564
585
  */
565
- function featureToOlFeature(feature) {
586
+ function featureToOlFeature(feature, options) {
587
+ const sourceProj = options?.sourceProjection ?? 'EPSG:4326';
588
+ const targetProj = options?.targetProjection ?? 'EPSG:3857';
566
589
  const geom = feature.geometry;
567
590
  let geometry;
568
591
  if (!geom.coordinates) {
@@ -570,14 +593,14 @@ function featureToOlFeature(feature) {
570
593
  }
571
594
  else if (geom.type === 'Point') {
572
595
  const coords = geom.coordinates;
573
- geometry = new Point(fromLonLat(coords));
596
+ geometry = new Point(transformCoords(coords, sourceProj, targetProj));
574
597
  }
575
598
  else if (geom.type === 'LineString') {
576
- const coords = geom.coordinates.map((c) => fromLonLat(c));
599
+ const coords = transformCoords(geom.coordinates, sourceProj, targetProj);
577
600
  geometry = new LineString(coords);
578
601
  }
579
602
  else if (geom.type === 'Polygon') {
580
- const rings = geom.coordinates.map((ring) => ring.map((c) => fromLonLat(c)));
603
+ const rings = transformCoords(geom.coordinates, sourceProj, targetProj);
581
604
  geometry = new Polygon(rings);
582
605
  }
583
606
  else {
@@ -166,12 +166,13 @@ function buildVectorLayer(config, source) {
166
166
  layer.set('id', config.id);
167
167
  layer.set('cluster-config', clusterCfg);
168
168
  layer.set('style-fn', styleFn);
169
+ layer.set('coordinate-projection', config.coordinateProjection);
169
170
  return layer;
170
171
  }
171
172
  function buildHeatmapLayer(config) {
172
173
  const vectorSource = new VectorSource();
173
174
  if (config.features && config.features.length > 0) {
174
- const olFeatures = config.features.map(featureToOlFeature);
175
+ const olFeatures = config.features.map((f) => featureToOlFeature(f));
175
176
  vectorSource.addFeatures(olFeatures);
176
177
  }
177
178
  const layer = new HeatmapLayer({
@@ -392,9 +393,16 @@ class OlLayerService {
392
393
  sourceOptions.format = new KML();
393
394
  }
394
395
  const vectorSource = new VectorSource(sourceOptions);
396
+ const targetProj = (typeof map.getView === 'function'
397
+ ? map.getView()?.getProjection()?.getCode()
398
+ : undefined) ?? 'EPSG:3857';
399
+ const sourceProj = vConfig.coordinateProjection ?? 'EPSG:4326';
395
400
  if (vConfig.features && vConfig.features.length > 0) {
396
401
  const olFeatures = vConfig.features.map((f) => {
397
- const olf = featureToOlFeature(f);
402
+ const olf = featureToOlFeature(f, {
403
+ sourceProjection: sourceProj,
404
+ targetProjection: targetProj,
405
+ });
398
406
  if (f.style)
399
407
  olf.set(STYLE_PROP, f.style);
400
408
  return olf;
@@ -462,6 +470,28 @@ class OlLayerService {
462
470
  const layer = this.layerCache.get(id);
463
471
  if (map && layer) {
464
472
  map.removeLayer(layer);
473
+ // Explicitly dispose sources to prevent memory leaks
474
+ if ('getSource' in layer) {
475
+ const source = layer.getSource();
476
+ if (source) {
477
+ // If it's a ClusterSource, dispose the underlying source first
478
+ if ('getSource' in source && typeof source.getSource === 'function') {
479
+ const underlyingSource = source.getSource();
480
+ if (underlyingSource && typeof underlyingSource.dispose === 'function') {
481
+ if (typeof underlyingSource.clear === 'function') {
482
+ underlyingSource.clear(true);
483
+ }
484
+ underlyingSource.dispose();
485
+ }
486
+ }
487
+ if (typeof source.dispose === 'function') {
488
+ if (typeof source.clear === 'function') {
489
+ source.clear(true);
490
+ }
491
+ source.dispose();
492
+ }
493
+ }
494
+ }
465
495
  layer.dispose();
466
496
  this.layerCache.delete(id);
467
497
  this.updateLayerState();
@@ -580,6 +610,11 @@ class OlLayerService {
580
610
  const layer = this.layerCache.get(id);
581
611
  if (!(layer instanceof VectorLayer))
582
612
  return;
613
+ const sourceProj = layer.get('coordinate-projection') ?? 'EPSG:4326';
614
+ const map = this.mapService.getMap();
615
+ const targetProj = (map && typeof map.getView === 'function'
616
+ ? map.getView()?.getProjection()?.getCode()
617
+ : undefined) ?? 'EPSG:3857';
583
618
  const source = layer.getSource();
584
619
  if (!source)
585
620
  return;
@@ -605,7 +640,10 @@ class OlLayerService {
605
640
  const featuresToAdd = features.filter((f) => !existingIds.has(f.id));
606
641
  if (featuresToAdd.length > 0) {
607
642
  const olFeatures = featuresToAdd.map((f) => {
608
- const olf = featureToOlFeature(f);
643
+ const olf = featureToOlFeature(f, {
644
+ sourceProjection: sourceProj,
645
+ targetProjection: targetProj,
646
+ });
609
647
  if (f.style)
610
648
  olf.set(STYLE_PROP, f.style);
611
649
  return olf;
@@ -674,6 +712,7 @@ class OlVectorLayerComponent {
674
712
  style = input(...(ngDevMode ? [undefined, { debugName: "style" }] : /* istanbul ignore next */ []));
675
713
  cluster = input(...(ngDevMode ? [undefined, { debugName: "cluster" }] : /* istanbul ignore next */ []));
676
714
  clusterComponent = contentChild(OlClusterComponent, ...(ngDevMode ? [{ debugName: "clusterComponent" }] : /* istanbul ignore next */ []));
715
+ coordinateProjection = input('EPSG:4326', ...(ngDevMode ? [{ debugName: "coordinateProjection" }] : /* istanbul ignore next */ []));
677
716
  constructor() {
678
717
  // Initialize layer after DOM is ready
679
718
  afterNextRender(() => {
@@ -701,6 +740,7 @@ class OlVectorLayerComponent {
701
740
  visible: this.visible(),
702
741
  style: this.style(),
703
742
  cluster: resolvedClusterConfig,
743
+ coordinateProjection: this.coordinateProjection(),
704
744
  });
705
745
  });
706
746
  // Effect to sync features when input changes
@@ -737,7 +777,7 @@ class OlVectorLayerComponent {
737
777
  });
738
778
  }
739
779
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlVectorLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
740
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.13", type: OlVectorLayerComponent, isStandalone: true, selector: "ol-vector-layer", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, features: { classPropertyName: "features", publicName: "features", isSignal: true, isRequired: false, transformFunction: null }, url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, opacity: { classPropertyName: "opacity", publicName: "opacity", isSignal: true, isRequired: false, transformFunction: null }, visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, style: { classPropertyName: "style", publicName: "style", isSignal: true, isRequired: false, transformFunction: null }, cluster: { classPropertyName: "cluster", publicName: "cluster", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "clusterComponent", first: true, predicate: OlClusterComponent, descendants: true, isSignal: true }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
780
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.13", type: OlVectorLayerComponent, isStandalone: true, selector: "ol-vector-layer", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, features: { classPropertyName: "features", publicName: "features", isSignal: true, isRequired: false, transformFunction: null }, url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, opacity: { classPropertyName: "opacity", publicName: "opacity", isSignal: true, isRequired: false, transformFunction: null }, visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, style: { classPropertyName: "style", publicName: "style", isSignal: true, isRequired: false, transformFunction: null }, cluster: { classPropertyName: "cluster", publicName: "cluster", isSignal: true, isRequired: false, transformFunction: null }, coordinateProjection: { classPropertyName: "coordinateProjection", publicName: "coordinateProjection", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "clusterComponent", first: true, predicate: OlClusterComponent, descendants: true, isSignal: true }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
741
781
  }
742
782
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlVectorLayerComponent, decorators: [{
743
783
  type: Component,
@@ -746,7 +786,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
746
786
  template: '',
747
787
  changeDetection: ChangeDetectionStrategy.OnPush,
748
788
  }]
749
- }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], features: [{ type: i0.Input, args: [{ isSignal: true, alias: "features", required: false }] }], url: [{ type: i0.Input, args: [{ isSignal: true, alias: "url", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], opacity: [{ type: i0.Input, args: [{ isSignal: true, alias: "opacity", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], style: [{ type: i0.Input, args: [{ isSignal: true, alias: "style", required: false }] }], cluster: [{ type: i0.Input, args: [{ isSignal: true, alias: "cluster", required: false }] }], clusterComponent: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlClusterComponent), { isSignal: true }] }] } });
789
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], features: [{ type: i0.Input, args: [{ isSignal: true, alias: "features", required: false }] }], url: [{ type: i0.Input, args: [{ isSignal: true, alias: "url", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], opacity: [{ type: i0.Input, args: [{ isSignal: true, alias: "opacity", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], style: [{ type: i0.Input, args: [{ isSignal: true, alias: "style", required: false }] }], cluster: [{ type: i0.Input, args: [{ isSignal: true, alias: "cluster", required: false }] }], clusterComponent: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlClusterComponent), { isSignal: true }] }], coordinateProjection: [{ type: i0.Input, args: [{ isSignal: true, alias: "coordinateProjection", required: false }] }] } });
750
790
 
751
791
  // OlTileLayerComponent
752
792
  class OlTileLayerComponent {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-helpers/openlayers",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "Modern Angular wrapper for OpenLayers with modular architecture, standalone components, and hybrid template/programmatic API",
5
5
  "homepage": "https://gaspar1992.github.io/angular-helpers/docs/openlayers",
6
6
  "repository": {
@@ -94,6 +94,7 @@ declare class OlMapComponent {
94
94
  zoom: _angular_core.InputSignal<number>;
95
95
  rotation: _angular_core.InputSignal<number>;
96
96
  projection: _angular_core.InputSignal<string>;
97
+ coordinateProjection: _angular_core.InputSignal<string>;
97
98
  viewChange: _angular_core.OutputEmitterRef<ViewState>;
98
99
  mapClick: _angular_core.OutputEmitterRef<MapClickEvent>;
99
100
  mapDblClick: _angular_core.OutputEmitterRef<MapClickEvent>;
@@ -101,6 +102,8 @@ declare class OlMapComponent {
101
102
  private map?;
102
103
  private resizeObserver?;
103
104
  constructor();
105
+ private getProjectedCoordinate;
106
+ private getExternalCoordinate;
104
107
  private initMap;
105
108
  private destroyMap;
106
109
  private updateCenter;
@@ -108,7 +111,7 @@ declare class OlMapComponent {
108
111
  private updateRotation;
109
112
  private emitViewChange;
110
113
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlMapComponent, never>;
111
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlMapComponent, "ol-map", never, { "center": { "alias": "center"; "required": false; "isSignal": true; }; "zoom": { "alias": "zoom"; "required": false; "isSignal": true; }; "rotation": { "alias": "rotation"; "required": false; "isSignal": true; }; "projection": { "alias": "projection"; "required": false; "isSignal": true; }; }, { "viewChange": "viewChange"; "mapClick": "mapClick"; "mapDblClick": "mapDblClick"; }, never, ["*"], true, never>;
114
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlMapComponent, "ol-map", never, { "center": { "alias": "center"; "required": false; "isSignal": true; }; "zoom": { "alias": "zoom"; "required": false; "isSignal": true; }; "rotation": { "alias": "rotation"; "required": false; "isSignal": true; }; "projection": { "alias": "projection"; "required": false; "isSignal": true; }; "coordinateProjection": { "alias": "coordinateProjection"; "required": false; "isSignal": true; }; }, { "viewChange": "viewChange"; "mapClick": "mapClick"; "mapDblClick": "mapDblClick"; }, never, ["*"], true, never>;
112
115
  }
113
116
 
114
117
  /**
@@ -379,18 +382,23 @@ interface VectorResourceOptions {
379
382
  */
380
383
  declare function createVectorResource(url: Signal<string | undefined>, options?: VectorResourceOptions): Resource<Feature[]>;
381
384
 
385
+ interface ProjectionOptions {
386
+ sourceProjection?: string;
387
+ targetProjection?: string;
388
+ }
382
389
  /**
383
390
  * Converts an OpenLayers feature to the internal Feature format.
384
- * Handles coordinate extraction and geometry type mapping.
391
+ * Handles coordinate extraction and geometry type mapping with custom projections.
385
392
  *
386
393
  * @param olFeature - The OpenLayers feature to convert
394
+ * @param options - Projection source and target codes
387
395
  * @returns The converted Feature with normalized structure
388
396
  */
389
- declare function olFeatureToFeature(olFeature: Feature$1): Feature;
397
+ declare function olFeatureToFeature(olFeature: Feature$1, options?: ProjectionOptions): Feature;
390
398
  /**
391
399
  * Converts an internal Feature to an OpenLayers feature.
392
400
  */
393
- declare function featureToOlFeature(feature: Feature): Feature$1;
401
+ declare function featureToOlFeature(feature: Feature, options?: ProjectionOptions): Feature$1;
394
402
 
395
403
  type OlFeatureKind = 'layers' | 'controls' | 'interactions' | 'overlays' | 'military' | 'projections';
396
404
  interface OlFeature<Kind extends OlFeatureKind> {
@@ -33,6 +33,7 @@ interface VectorLayerConfig extends LayerConfig {
33
33
  format?: 'geojson' | 'topojson' | 'kml';
34
34
  style?: Style | ((feature: Feature) => Style);
35
35
  cluster?: ClusterConfig;
36
+ coordinateProjection?: string;
36
37
  }
37
38
  interface HeatmapLayerConfig extends LayerConfig {
38
39
  type: 'heatmap';
@@ -87,9 +88,10 @@ declare class OlVectorLayerComponent {
87
88
  style: _angular_core.InputSignal<any>;
88
89
  cluster: _angular_core.InputSignal<ClusterConfig>;
89
90
  clusterComponent: _angular_core.Signal<OlClusterComponent>;
91
+ coordinateProjection: _angular_core.InputSignal<string>;
90
92
  constructor();
91
93
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlVectorLayerComponent, never>;
92
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlVectorLayerComponent, "ol-vector-layer", never, { "id": { "alias": "id"; "required": true; "isSignal": true; }; "features": { "alias": "features"; "required": false; "isSignal": true; }; "url": { "alias": "url"; "required": false; "isSignal": true; }; "format": { "alias": "format"; "required": false; "isSignal": true; }; "zIndex": { "alias": "zIndex"; "required": false; "isSignal": true; }; "opacity": { "alias": "opacity"; "required": false; "isSignal": true; }; "visible": { "alias": "visible"; "required": false; "isSignal": true; }; "style": { "alias": "style"; "required": false; "isSignal": true; }; "cluster": { "alias": "cluster"; "required": false; "isSignal": true; }; }, {}, ["clusterComponent"], never, true, never>;
94
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlVectorLayerComponent, "ol-vector-layer", never, { "id": { "alias": "id"; "required": true; "isSignal": true; }; "features": { "alias": "features"; "required": false; "isSignal": true; }; "url": { "alias": "url"; "required": false; "isSignal": true; }; "format": { "alias": "format"; "required": false; "isSignal": true; }; "zIndex": { "alias": "zIndex"; "required": false; "isSignal": true; }; "opacity": { "alias": "opacity"; "required": false; "isSignal": true; }; "visible": { "alias": "visible"; "required": false; "isSignal": true; }; "style": { "alias": "style"; "required": false; "isSignal": true; }; "cluster": { "alias": "cluster"; "required": false; "isSignal": true; }; "coordinateProjection": { "alias": "coordinateProjection"; "required": false; "isSignal": true; }; }, {}, ["clusterComponent"], never, true, never>;
93
95
  }
94
96
 
95
97
  declare class OlTileLayerComponent {