@angular-helpers/openlayers 0.5.0 → 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.
@@ -1,44 +1,358 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, signal, computed, Injectable, input, ChangeDetectionStrategy, Component, DestroyRef, contentChild, afterNextRender, effect } from '@angular/core';
2
+ import { inject, signal, computed, Injectable, input, output, ChangeDetectionStrategy, Component, DestroyRef, contentChild, afterNextRender, effect } from '@angular/core';
3
3
  import VectorLayer from 'ol/layer/Vector';
4
4
  import HeatmapLayer from 'ol/layer/Heatmap';
5
5
  import TileLayer from 'ol/layer/Tile';
6
6
  import ImageLayer from 'ol/layer/Image';
7
7
  import VectorSource from 'ol/source/Vector';
8
- import OLFeature from 'ol/Feature';
9
- import { Point, LineString, Polygon, Circle } from 'ol/geom';
10
- import { fromLonLat } from 'ol/proj';
11
- import { getCenter } from 'ol/extent';
12
- import { Style, Circle as Circle$1, Stroke, Fill, Icon } from 'ol/style';
13
- import Text from 'ol/style/Text';
14
8
  import ClusterSource from 'ol/source/Cluster';
9
+ import GeoJSON from 'ol/format/GeoJSON';
10
+ import TopoJSON from 'ol/format/TopoJSON';
11
+ import KML from 'ol/format/KML';
12
+ import { getCenter } from 'ol/extent';
13
+ import { Point, LineString, Polygon, Circle as Circle$1 } from 'ol/geom';
14
+ import { featureToOlFeature, olFeatureToFeature, OlMapService } from '@angular-helpers/openlayers/core';
15
15
  import OSM from 'ol/source/OSM';
16
16
  import XYZ from 'ol/source/XYZ';
17
17
  import TileWMS from 'ol/source/TileWMS';
18
18
  import ImageWMS from 'ol/source/ImageWMS';
19
19
  import ImageStatic from 'ol/source/ImageStatic';
20
- import GeoJSON from 'ol/format/GeoJSON';
21
- import TopoJSON from 'ol/format/TopoJSON';
22
- import KML from 'ol/format/KML';
23
- import { OlMapService } from '@angular-helpers/openlayers/core';
20
+ import { Style, Text, Fill, Circle, Stroke, Icon } from 'ol/style';
21
+ import OLFeature from 'ol/Feature';
22
+ import { fromLonLat } from 'ol/proj';
24
23
  import WebGLVectorLayer from 'ol/layer/WebGLVector';
25
24
  import WebGLTileLayer from 'ol/layer/WebGLTile';
26
25
  import WebGLVectorTileLayer from 'ol/layer/WebGLVectorTile';
27
26
  import VectorTileSource from 'ol/source/VectorTile';
28
27
  import MVT from 'ol/format/MVT';
29
28
 
30
- // OlLayerService
31
- /**
32
- * Internal property key used to stash the abstract style metadata on the
33
- * underlying `ol/Feature` so the layer style function can resolve a
34
- * per-feature visual without colliding with user `properties`.
35
- */
29
+ function buildTileSource(config) {
30
+ switch (config.type) {
31
+ case 'osm':
32
+ return new OSM({ attributions: config.attributions });
33
+ case 'xyz':
34
+ return new XYZ({ url: config.url, attributions: config.attributions });
35
+ case 'wms':
36
+ return new TileWMS({
37
+ url: config.url,
38
+ params: config.params ?? {},
39
+ attributions: config.attributions,
40
+ });
41
+ default:
42
+ return new OSM();
43
+ }
44
+ }
45
+ function buildImageSource(config) {
46
+ if (config.type === 'static') {
47
+ return new ImageStatic({
48
+ url: config.url,
49
+ imageExtent: config.imageExtent ?? [0, 0, 1, 1],
50
+ });
51
+ }
52
+ return new ImageWMS({ url: config.url, params: config.params });
53
+ }
54
+
55
+ function createClusterStyleFn(clusterCfg, styleFn, defaultStyle) {
56
+ return (olFeature, resolution) => {
57
+ const features = olFeature.get('features');
58
+ const size = features ? features.length : 0;
59
+ // Render cluster badge
60
+ if (size > 1) {
61
+ const showCount = clusterCfg?.showCount ?? true;
62
+ return new Style({
63
+ image: new Circle({
64
+ radius: 15 + Math.min(size * 2, 15),
65
+ fill: new Fill({ color: 'rgba(255, 100, 100, 0.8)' }),
66
+ stroke: new Stroke({ color: '#fff', width: 2 }),
67
+ }),
68
+ text: showCount
69
+ ? new Text({
70
+ text: String(size),
71
+ fill: new Fill({ color: '#fff' }),
72
+ })
73
+ : undefined,
74
+ });
75
+ }
76
+ // Single feature in cluster: unwrap and call styleFn
77
+ const originalFeature = features?.[0];
78
+ if (originalFeature) {
79
+ // 1. Feature level override
80
+ if (clusterCfg?.featureStyle) {
81
+ // OpenLayers styles are mutable. Clone before modifying geometry to prevent global breakage.
82
+ const style = clusterCfg.featureStyle; // Cast since it might be abstract
83
+ if (style instanceof Style) {
84
+ const origGeom = originalFeature.getGeometry();
85
+ if (origGeom) {
86
+ const clonedStyle = style.clone();
87
+ clonedStyle.setGeometry(origGeom);
88
+ return clonedStyle;
89
+ }
90
+ }
91
+ return style;
92
+ }
93
+ // 2. Original Layer style
94
+ if (styleFn) {
95
+ const style = styleFn(originalFeature, resolution);
96
+ if (style instanceof Style) {
97
+ const origGeom = originalFeature.getGeometry();
98
+ if (origGeom) {
99
+ const clonedStyle = style.clone();
100
+ clonedStyle.setGeometry(origGeom);
101
+ return clonedStyle;
102
+ }
103
+ }
104
+ return style;
105
+ }
106
+ }
107
+ // 3. Fallback to styleFn or default
108
+ return styleFn ? styleFn(olFeature, resolution) : defaultStyle;
109
+ };
110
+ }
111
+
112
+ const STYLE_PROP$1 = '__angular_helpers_style__';
113
+ const defaultStyle = new Style({
114
+ image: new Circle({
115
+ radius: 6,
116
+ fill: new Fill({ color: '#3399CC' }),
117
+ stroke: new Stroke({ color: '#fff', width: 2 }),
118
+ }),
119
+ });
120
+ function buildVectorLayer(config, source) {
121
+ const clusterCfg = config.cluster;
122
+ const userStyle = config.style;
123
+ const styleFn = (olFeature, resolution) => {
124
+ const abstractStyle = olFeature.get(STYLE_PROP$1);
125
+ if (abstractStyle) {
126
+ const style = new Style();
127
+ const { icon, fill, stroke } = abstractStyle;
128
+ if (icon?.src) {
129
+ style.setImage(new Icon({
130
+ src: icon.src,
131
+ ...(icon.size ? { size: icon.size } : {}),
132
+ ...(icon.anchor ? { anchor: icon.anchor } : {}),
133
+ }));
134
+ }
135
+ if (fill)
136
+ style.setFill(new Fill({ color: fill.color }));
137
+ if (stroke)
138
+ style.setStroke(new Stroke({ color: stroke.color, width: stroke.width }));
139
+ if (icon?.src || fill || stroke)
140
+ return style;
141
+ }
142
+ if (userStyle) {
143
+ if (typeof userStyle === 'function') {
144
+ const feature = {
145
+ id: String(olFeature.getId() ?? ''),
146
+ geometry: {
147
+ type: olFeature.getGeometry()?.getType(),
148
+ coordinates: [],
149
+ },
150
+ properties: olFeature.getProperties(),
151
+ };
152
+ return userStyle(feature, resolution);
153
+ }
154
+ return userStyle;
155
+ }
156
+ return defaultStyle;
157
+ };
158
+ const clusterStyleFn = createClusterStyleFn(clusterCfg, styleFn, defaultStyle);
159
+ const layer = new VectorLayer({
160
+ source,
161
+ visible: config.visible ?? true,
162
+ opacity: config.opacity ?? 1,
163
+ zIndex: config.zIndex,
164
+ style: clusterCfg?.enabled ? clusterStyleFn : styleFn,
165
+ });
166
+ layer.set('id', config.id);
167
+ layer.set('cluster-config', clusterCfg);
168
+ layer.set('style-fn', styleFn);
169
+ layer.set('coordinate-projection', config.coordinateProjection);
170
+ return layer;
171
+ }
172
+ function buildHeatmapLayer(config) {
173
+ const vectorSource = new VectorSource();
174
+ if (config.features && config.features.length > 0) {
175
+ const olFeatures = config.features.map((f) => featureToOlFeature(f));
176
+ vectorSource.addFeatures(olFeatures);
177
+ }
178
+ const layer = new HeatmapLayer({
179
+ source: vectorSource,
180
+ visible: config.visible ?? true,
181
+ opacity: config.opacity ?? 1,
182
+ zIndex: config.zIndex,
183
+ ...(config.blur !== undefined && { blur: config.blur }),
184
+ ...(config.radius !== undefined && { radius: config.radius }),
185
+ ...(config.weight !== undefined && {
186
+ weight: config.weight,
187
+ }),
188
+ });
189
+ layer.set('id', config.id);
190
+ return layer;
191
+ }
192
+ function buildTileLayer(config, source) {
193
+ const layer = new TileLayer({
194
+ source,
195
+ visible: config.visible ?? true,
196
+ opacity: config.opacity ?? 1,
197
+ zIndex: config.zIndex,
198
+ });
199
+ layer.set('id', config.id);
200
+ return layer;
201
+ }
202
+ function buildImageLayer(config, source) {
203
+ const layer = new ImageLayer({
204
+ source,
205
+ visible: config.visible ?? true,
206
+ opacity: config.opacity ?? 1,
207
+ zIndex: config.zIndex,
208
+ });
209
+ layer.set('id', config.id);
210
+ return layer;
211
+ }
212
+
213
+ class SpiderficationManager {
214
+ spiderSource = new VectorSource();
215
+ spiderLayer = new VectorLayer({
216
+ source: this.spiderSource,
217
+ zIndex: 9999,
218
+ properties: { 'is-spider-layer': true },
219
+ });
220
+ mapClickListenerRegistered = false;
221
+ map = null;
222
+ layerCache;
223
+ constructor(layerCache) {
224
+ this.layerCache = layerCache;
225
+ }
226
+ register(map) {
227
+ if (this.mapClickListenerRegistered)
228
+ return;
229
+ this.mapClickListenerRegistered = true;
230
+ this.map = map;
231
+ map.addLayer(this.spiderLayer);
232
+ // Unspiderfy on map movement or zoom
233
+ map.on('movestart', () => this.unspiderfy());
234
+ map.on('singleclick', (e) => {
235
+ let handled = false;
236
+ let keepSpiderOpen = false;
237
+ map.forEachFeatureAtPixel(e.pixel, (f, l) => {
238
+ if (handled)
239
+ return;
240
+ // Check if we clicked a spider item
241
+ if (l === this.spiderLayer) {
242
+ const originalOlFeature = f.get('spider-feature');
243
+ if (originalOlFeature) {
244
+ const layerId = f.get('cluster-layer-id');
245
+ const layerObj = this.layerCache.get(layerId);
246
+ if (layerObj) {
247
+ const clusterCfg = layerObj.get('cluster-config');
248
+ if (clusterCfg?.onSpiderfyClick) {
249
+ // Use olFeatureToFeature so coordinates are properly extracted!
250
+ const feat = olFeatureToFeature(originalOlFeature);
251
+ clusterCfg.onSpiderfyClick(feat);
252
+ }
253
+ }
254
+ }
255
+ handled = true;
256
+ keepSpiderOpen = true; // Keep spider open when clicking a leg
257
+ return;
258
+ }
259
+ // Check if we clicked a cluster
260
+ if (!l)
261
+ return;
262
+ const features = f.get('features');
263
+ if (features && features.length > 1) {
264
+ const clusterCfg = l.get('cluster-config');
265
+ if (clusterCfg?.spiderfyOnSelect) {
266
+ keepSpiderOpen = true;
267
+ handled = true;
268
+ // Execute layer manipulations outside the synchronous event loop
269
+ setTimeout(() => {
270
+ this.spiderfy(map, f, features, l, clusterCfg);
271
+ });
272
+ }
273
+ }
274
+ });
275
+ // Cleanup existing spider layer if we clicked anything else
276
+ if (!keepSpiderOpen) {
277
+ this.unspiderfy();
278
+ }
279
+ });
280
+ }
281
+ unspiderfy() {
282
+ this.spiderSource.clear();
283
+ }
284
+ spiderfy(map, clusterFeature, features, parentLayer, cfg) {
285
+ this.unspiderfy();
286
+ const count = features.length;
287
+ const centerGeom = clusterFeature.getGeometry();
288
+ if (!centerGeom || centerGeom.getType() !== 'Point')
289
+ return;
290
+ const centerCoords = centerGeom.getCoordinates();
291
+ const resolution = map.getView().getResolution() ?? 1;
292
+ const baseRadius = 30; // 30 pixels
293
+ const radius = baseRadius + count * 2;
294
+ const angleStep = (2 * Math.PI) / count;
295
+ const spiderFeatures = [];
296
+ const styleFn = parentLayer.get('style-fn');
297
+ features.forEach((f, i) => {
298
+ let x, y;
299
+ if (count <= 8) {
300
+ // Circle layout for small number of features
301
+ const angle = i * angleStep;
302
+ x = centerCoords[0] + radius * Math.cos(angle) * resolution;
303
+ y = centerCoords[1] + radius * Math.sin(angle) * resolution;
304
+ }
305
+ else {
306
+ // Spiral layout (caracol) for many features
307
+ const initialRadius = 20;
308
+ const legLength = 15;
309
+ const spiralAngleStep = 0.5;
310
+ const angle = i * spiralAngleStep;
311
+ const r = initialRadius + legLength * (angle / Math.PI);
312
+ x = centerCoords[0] + r * Math.cos(angle) * resolution;
313
+ y = centerCoords[1] + r * Math.sin(angle) * resolution;
314
+ }
315
+ const legGeom = new Point([x, y]);
316
+ // Create leg line
317
+ const lineFeature = new OLFeature(new LineString([centerCoords, [x, y]]));
318
+ lineFeature.setStyle(new Style({ stroke: new Stroke({ color: 'rgba(0,0,0,0.5)', width: 2 }) }));
319
+ spiderFeatures.push(lineFeature);
320
+ // Create point feature
321
+ const pointFeature = new OLFeature(legGeom);
322
+ // Determine style for the spider leg point
323
+ let pointStyle = undefined;
324
+ if (cfg?.featureStyle) {
325
+ pointStyle = cfg.featureStyle;
326
+ }
327
+ else if (styleFn) {
328
+ pointStyle = styleFn(f, resolution);
329
+ }
330
+ // Ensure we always have a visible style even if the original feature has none
331
+ if (!pointStyle || (Array.isArray(pointStyle) && pointStyle.length === 0)) {
332
+ pointStyle = new Style({
333
+ image: new Circle({
334
+ radius: 6,
335
+ fill: new Fill({ color: '#3399CC' }),
336
+ stroke: new Stroke({ color: '#fff', width: 2 }),
337
+ }),
338
+ });
339
+ }
340
+ pointFeature.setStyle(pointStyle);
341
+ pointFeature.set('spider-feature', f);
342
+ pointFeature.set('cluster-layer-id', parentLayer.get('id'));
343
+ spiderFeatures.push(pointFeature);
344
+ });
345
+ this.spiderSource.addFeatures(spiderFeatures);
346
+ }
347
+ }
348
+
36
349
  const STYLE_PROP = '__angular_helpers_style__';
37
350
  class OlLayerService {
38
351
  mapService = inject(OlMapService);
39
352
  layerCache = new Map();
40
353
  pendingConfigs = [];
41
354
  layerState = signal([], ...(ngDevMode ? [{ debugName: "layerState" }] : /* istanbul ignore next */ []));
355
+ spiderManager = new SpiderficationManager(this.layerCache);
42
356
  layers = computed(() => this.layerState(), ...(ngDevMode ? [{ debugName: "layers" }] : /* istanbul ignore next */ []));
43
357
  visibleLayers = computed(() => this.layerState().filter((l) => l.visible), ...(ngDevMode ? [{ debugName: "visibleLayers" }] : /* istanbul ignore next */ []));
44
358
  tileLayers = computed(() => this.layerState().filter((l) => l.type === 'tile'), ...(ngDevMode ? [{ debugName: "tileLayers" }] : /* istanbul ignore next */ []));
@@ -64,18 +378,81 @@ class OlLayerService {
64
378
  }
65
379
  }
66
380
  createLayer(config, map) {
381
+ let layer;
67
382
  switch (config.type) {
68
- case 'vector':
69
- return this.createVectorLayer(config, map);
383
+ case 'vector': {
384
+ const vConfig = config;
385
+ const sourceOptions = {};
386
+ if (vConfig.url && vConfig.format) {
387
+ sourceOptions.url = vConfig.url;
388
+ if (vConfig.format === 'geojson')
389
+ sourceOptions.format = new GeoJSON();
390
+ else if (vConfig.format === 'topojson')
391
+ sourceOptions.format = new TopoJSON();
392
+ else if (vConfig.format === 'kml')
393
+ sourceOptions.format = new KML();
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';
400
+ if (vConfig.features && vConfig.features.length > 0) {
401
+ const olFeatures = vConfig.features.map((f) => {
402
+ const olf = featureToOlFeature(f, {
403
+ sourceProjection: sourceProj,
404
+ targetProjection: targetProj,
405
+ });
406
+ if (f.style)
407
+ olf.set(STYLE_PROP, f.style);
408
+ return olf;
409
+ });
410
+ vectorSource.addFeatures(olFeatures);
411
+ }
412
+ const clusterCfg = vConfig.cluster;
413
+ const source = clusterCfg?.enabled
414
+ ? new ClusterSource({
415
+ source: vectorSource,
416
+ distance: clusterCfg.distance ?? 40,
417
+ minDistance: clusterCfg.minDistance ?? 20,
418
+ geometryFunction: (feature) => {
419
+ const geometry = feature.getGeometry();
420
+ if (!geometry)
421
+ return null;
422
+ if (geometry.getType() === 'Point')
423
+ return geometry;
424
+ return new Point(getCenter(geometry.getExtent()));
425
+ },
426
+ })
427
+ : vectorSource;
428
+ layer = buildVectorLayer(vConfig, source);
429
+ if (clusterCfg?.spiderfyOnSelect) {
430
+ this.spiderManager.register(map);
431
+ }
432
+ break;
433
+ }
70
434
  case 'heatmap':
71
- return this.createHeatmapLayer(config, map);
72
- case 'tile':
73
- return this.createTileLayer(config, map);
74
- case 'image':
75
- return this.createImageLayer(config, map);
435
+ layer = buildHeatmapLayer(config);
436
+ break;
437
+ case 'tile': {
438
+ const tConfig = config;
439
+ const source = buildTileSource(tConfig.source);
440
+ layer = buildTileLayer(tConfig, source);
441
+ break;
442
+ }
443
+ case 'image': {
444
+ const iConfig = config;
445
+ const source = buildImageSource(iConfig.source);
446
+ layer = buildImageLayer(iConfig, source);
447
+ break;
448
+ }
76
449
  default:
77
450
  return { id: config.id };
78
451
  }
452
+ map.addLayer(layer);
453
+ this.layerCache.set(config.id, layer);
454
+ this.updateLayerState();
455
+ return { id: config.id };
79
456
  }
80
457
  getLayer(id) {
81
458
  return this.layerCache.get(id);
@@ -84,7 +461,6 @@ class OlLayerService {
84
461
  return this.layerCache.has(id);
85
462
  }
86
463
  removeLayer(id) {
87
- // Cancel if it's still pending (map not ready yet)
88
464
  const pendingIdx = this.pendingConfigs.findIndex((c) => c.id === id);
89
465
  if (pendingIdx !== -1) {
90
466
  this.pendingConfigs.splice(pendingIdx, 1);
@@ -94,6 +470,29 @@ class OlLayerService {
94
470
  const layer = this.layerCache.get(id);
95
471
  if (map && layer) {
96
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
+ }
495
+ layer.dispose();
97
496
  this.layerCache.delete(id);
98
497
  this.updateLayerState();
99
498
  }
@@ -106,9 +505,8 @@ class OlLayerService {
106
505
  }
107
506
  else {
108
507
  const pending = this.pendingConfigs.find((c) => c.id === id);
109
- if (pending) {
508
+ if (pending)
110
509
  pending.visible = visible;
111
- }
112
510
  }
113
511
  }
114
512
  toggleVisibility(id) {
@@ -129,9 +527,8 @@ class OlLayerService {
129
527
  }
130
528
  else {
131
529
  const pending = this.pendingConfigs.find((c) => c.id === id);
132
- if (pending) {
530
+ if (pending)
133
531
  pending.opacity = opacity;
134
- }
135
532
  }
136
533
  }
137
534
  setZIndex(id, zIndex) {
@@ -142,8 +539,25 @@ class OlLayerService {
142
539
  }
143
540
  else {
144
541
  const pending = this.pendingConfigs.find((c) => c.id === id);
145
- if (pending) {
542
+ if (pending)
146
543
  pending.zIndex = zIndex;
544
+ }
545
+ }
546
+ setClusterDistance(id, distance) {
547
+ const layer = this.layerCache.get(id);
548
+ if (layer instanceof VectorLayer) {
549
+ const source = layer.getSource();
550
+ if (source && 'setDistance' in source) {
551
+ source.setDistance(distance);
552
+ }
553
+ }
554
+ }
555
+ setClusterMinDistance(id, minDistance) {
556
+ const layer = this.layerCache.get(id);
557
+ if (layer instanceof VectorLayer) {
558
+ const source = layer.getSource();
559
+ if (source && 'setMinDistance' in source) {
560
+ source.setMinDistance(minDistance);
147
561
  }
148
562
  }
149
563
  }
@@ -179,11 +593,6 @@ class OlLayerService {
179
593
  getZIndex(id) {
180
594
  return this.layerCache.get(id)?.getZIndex() ?? 0;
181
595
  }
182
- /**
183
- * Clears all features from a vector layer's source.
184
- * Does not remove the layer itself.
185
- * @param id - Layer identifier
186
- */
187
596
  clearFeatures(id) {
188
597
  const layer = this.layerCache.get(id);
189
598
  if (!(layer instanceof VectorLayer))
@@ -191,85 +600,53 @@ class OlLayerService {
191
600
  const source = layer.getSource();
192
601
  if (!source)
193
602
  return;
194
- // Handle Cluster source: clear the underlying VectorSource
195
603
  const clusterSource = source;
196
604
  const vectorSource = clusterSource.getSource
197
605
  ? clusterSource.getSource()
198
606
  : source;
199
607
  vectorSource?.clear();
200
608
  }
201
- /**
202
- * Updates the features of a vector layer.
203
- * Syncs new features without clearing existing ones (preserves OL modifications).
204
- * @param id - Layer identifier
205
- * @param features - New features to sync
206
- */
207
609
  updateFeatures(id, features) {
208
610
  const layer = this.layerCache.get(id);
209
611
  if (!(layer instanceof VectorLayer))
210
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';
211
618
  const source = layer.getSource();
212
619
  if (!source)
213
620
  return;
214
- // Handle Cluster source: get the underlying VectorSource
215
621
  const clusterSource = source;
216
622
  const vectorSource = clusterSource.getSource
217
623
  ? clusterSource.getSource()
218
624
  : source;
219
625
  if (!(vectorSource instanceof VectorSource))
220
626
  return;
221
- // Sync features: remove old ones, update existing ones, add new ones
222
627
  if (features) {
223
628
  const newFeatureIds = new Set(features.map((f) => f.id));
224
629
  const sourceFeatures = vectorSource.getFeatures();
225
- // 1. Remove features that are no longer in the input
226
630
  sourceFeatures.forEach((f) => {
227
- const id = f.getId();
228
- if (id !== undefined && !newFeatureIds.has(id)) {
631
+ const fId = f.getId();
632
+ if (fId !== undefined && !newFeatureIds.has(fId)) {
229
633
  vectorSource.removeFeature(f);
230
634
  }
231
635
  });
232
- // 2. Only add features that don't already exist in the source
233
636
  const existingIds = new Set(vectorSource
234
637
  .getFeatures()
235
638
  .map((f) => f.getId())
236
- .filter((id) => id !== undefined));
639
+ .filter((fId) => fId !== undefined));
237
640
  const featuresToAdd = features.filter((f) => !existingIds.has(f.id));
238
641
  if (featuresToAdd.length > 0) {
239
- const olFeatures = featuresToAdd.map((feature) => {
240
- const geom = feature.geometry;
241
- let geometry;
242
- if (!geom.coordinates) {
243
- geometry = new Point([0, 0]);
244
- }
245
- else if (geom.type === 'Point') {
246
- const coords = geom.coordinates;
247
- geometry = new Point(fromLonLat(coords));
248
- }
249
- else if (geom.type === 'LineString') {
250
- const coords = geom.coordinates.map((c) => fromLonLat(c));
251
- geometry = new LineString(coords);
252
- }
253
- else if (geom.type === 'Polygon') {
254
- const rings = geom.coordinates.map((ring) => ring.map((c) => fromLonLat(c)));
255
- geometry = new Polygon(rings);
256
- }
257
- else if (geom.type === 'Circle') {
258
- const center = fromLonLat(geom.coordinates);
259
- geometry = new Circle(center, geom.radius ?? 1000);
260
- }
261
- else {
262
- geometry = new Point([0, 0]);
263
- }
264
- const olFeature = new OLFeature({
265
- geometry,
266
- ...feature.properties,
642
+ const olFeatures = featuresToAdd.map((f) => {
643
+ const olf = featureToOlFeature(f, {
644
+ sourceProjection: sourceProj,
645
+ targetProjection: targetProj,
267
646
  });
268
- if (feature.style) {
269
- olFeature.set(STYLE_PROP, feature.style);
270
- }
271
- olFeature.setId(feature.id);
272
- return olFeature;
647
+ if (f.style)
648
+ olf.set(STYLE_PROP, f.style);
649
+ return olf;
273
650
  });
274
651
  vectorSource.addFeatures(olFeatures);
275
652
  }
@@ -295,285 +672,6 @@ class OlLayerService {
295
672
  });
296
673
  this.layerState.set(layers.sort((a, b) => a.zIndex - b.zIndex));
297
674
  }
298
- createVectorLayer(config, map) {
299
- const sourceOptions = {};
300
- if (config.url && config.format) {
301
- sourceOptions.url = config.url;
302
- if (config.format === 'geojson')
303
- sourceOptions.format = new GeoJSON();
304
- else if (config.format === 'topojson')
305
- sourceOptions.format = new TopoJSON();
306
- else if (config.format === 'kml')
307
- sourceOptions.format = new KML();
308
- }
309
- const vectorSource = new VectorSource(sourceOptions);
310
- // Add features if provided
311
- if (config.features && config.features.length > 0) {
312
- const olFeatures = config.features.map((feature) => {
313
- const geom = feature.geometry;
314
- let geometry;
315
- // Validate coordinates exist before processing
316
- if (!geom.coordinates) {
317
- geometry = new Point([0, 0]);
318
- }
319
- else if (geom.type === 'Point') {
320
- // Transform from EPSG:4326 (lon/lat) to EPSG:3857 (map projection)
321
- const coords = geom.coordinates;
322
- geometry = new Point(fromLonLat(coords));
323
- }
324
- else if (geom.type === 'LineString') {
325
- const coords = geom.coordinates.map((c) => fromLonLat(c));
326
- geometry = new LineString(coords);
327
- }
328
- else if (geom.type === 'Polygon') {
329
- const rings = geom.coordinates.map((ring) => ring.map((c) => fromLonLat(c)));
330
- geometry = new Polygon(rings);
331
- }
332
- else if (geom.type === 'Circle') {
333
- const center = fromLonLat(geom.coordinates);
334
- geometry = new Circle(center, geom.radius ?? 1000);
335
- }
336
- else {
337
- geometry = new Point([0, 0]);
338
- }
339
- const olFeature = new OLFeature({
340
- geometry,
341
- ...feature.properties,
342
- });
343
- if (feature.style) {
344
- olFeature.set(STYLE_PROP, feature.style);
345
- }
346
- olFeature.setId(feature.id);
347
- return olFeature;
348
- });
349
- vectorSource.addFeatures(olFeatures);
350
- }
351
- // Wrap in cluster source if enabled
352
- const clusterCfg = config.cluster;
353
- const source = clusterCfg?.enabled
354
- ? new ClusterSource({
355
- source: vectorSource,
356
- distance: clusterCfg.distance ?? 40,
357
- minDistance: clusterCfg.minDistance ?? 20,
358
- geometryFunction: (feature) => {
359
- const geometry = feature.getGeometry();
360
- if (!geometry)
361
- return null;
362
- // For Point geometries, use as-is
363
- if (geometry.getType() === 'Point') {
364
- return geometry;
365
- }
366
- // For other geometries (Polygon, Circle, etc.), use center point
367
- const extent = geometry.getExtent();
368
- const center = getCenter(extent);
369
- return new Point(center);
370
- },
371
- })
372
- : vectorSource;
373
- // Default style for all geometry types (points, lines, polygons)
374
- const defaultStyle = new Style({
375
- fill: new Fill({ color: 'rgba(25, 118, 210, 0.3)' }),
376
- stroke: new Stroke({ color: '#1976d2', width: 2 }),
377
- image: new Circle$1({
378
- radius: 8,
379
- fill: new Fill({ color: '#1976d2' }),
380
- stroke: new Stroke({ color: '#d32f2f', width: 2 }),
381
- }),
382
- });
383
- // Resolved style: priority to stashed metadata, then config-level style, then default.
384
- const userStyle = config.style;
385
- const styleFn = (olFeature, resolution) => {
386
- // 1. Per-feature style metadata (stashed via STYLE_PROP)
387
- const abstractStyle = olFeature.get(STYLE_PROP);
388
- if (abstractStyle) {
389
- const style = new Style();
390
- const { icon, fill, stroke } = abstractStyle;
391
- if (icon?.src) {
392
- style.setImage(new Icon({
393
- src: icon.src,
394
- ...(icon.size ? { size: icon.size } : {}),
395
- ...(icon.anchor ? { anchor: icon.anchor } : {}),
396
- }));
397
- }
398
- if (fill) {
399
- style.setFill(new Fill({ color: fill.color }));
400
- }
401
- if (stroke) {
402
- style.setStroke(new Stroke({ color: stroke.color, width: stroke.width }));
403
- }
404
- if (icon?.src || fill || stroke)
405
- return style;
406
- }
407
- // 2. Layer-level style from config (supports functions or static styles)
408
- if (userStyle) {
409
- if (typeof userStyle === 'function') {
410
- // Check if it's already an OL native feature or wrap if needed
411
- // For simplicity in the demo, we pass the feature as-is or mapped
412
- const feature = {
413
- id: String(olFeature.getId() ?? ''),
414
- geometry: {
415
- type: olFeature.getGeometry()?.getType(),
416
- coordinates: [], // coordinates not easily reversible without extra work
417
- },
418
- properties: olFeature.getProperties(),
419
- };
420
- return userStyle(feature, resolution);
421
- }
422
- return userStyle;
423
- }
424
- return defaultStyle;
425
- };
426
- // Cluster style: shows count badge when features are clustered, else delegates to styleFn
427
- const clusterStyleFn = (olFeature, resolution) => {
428
- const features = olFeature.get('features');
429
- const size = features?.length ?? 1;
430
- if (size > 1) {
431
- const showCount = clusterCfg?.showCount ?? true;
432
- return new Style({
433
- image: new Circle$1({
434
- radius: 15 + Math.min(size * 2, 15),
435
- fill: new Fill({ color: 'rgba(255, 100, 100, 0.8)' }),
436
- stroke: new Stroke({ color: '#fff', width: 2 }),
437
- }),
438
- text: showCount
439
- ? new Text({
440
- text: String(size),
441
- fill: new Fill({ color: '#fff' }),
442
- })
443
- : undefined,
444
- });
445
- }
446
- // Single feature in cluster: unwrap and call styleFn
447
- const originalFeature = features?.[0];
448
- if (originalFeature) {
449
- // We MUST preserve the original geometry if it's a non-point ( spiderfication etc)
450
- const style = styleFn(originalFeature, resolution);
451
- if (style instanceof Style) {
452
- const origGeom = originalFeature.getGeometry();
453
- if (origGeom)
454
- style.setGeometry(origGeom);
455
- }
456
- return style;
457
- }
458
- return styleFn(olFeature, resolution);
459
- };
460
- const layer = new VectorLayer({
461
- source,
462
- visible: config.visible ?? true,
463
- opacity: config.opacity ?? 1,
464
- zIndex: config.zIndex,
465
- style: clusterCfg?.enabled ? clusterStyleFn : styleFn,
466
- });
467
- layer.set('id', config.id);
468
- map.addLayer(layer);
469
- this.layerCache.set(config.id, layer);
470
- this.updateLayerState();
471
- return { id: config.id };
472
- }
473
- createHeatmapLayer(config, map) {
474
- const vectorSource = new VectorSource();
475
- if (config.features && config.features.length > 0) {
476
- const olFeatures = config.features.map((feature) => {
477
- const geom = feature.geometry;
478
- let geometry;
479
- if (!geom.coordinates) {
480
- geometry = new Point([0, 0]);
481
- }
482
- else if (geom.type === 'Point') {
483
- const coords = geom.coordinates;
484
- geometry = new Point(fromLonLat(coords));
485
- }
486
- else if (geom.type === 'LineString') {
487
- const coords = geom.coordinates.map((c) => fromLonLat(c));
488
- geometry = new LineString(coords);
489
- }
490
- else if (geom.type === 'Polygon') {
491
- const rings = geom.coordinates.map((ring) => ring.map((c) => fromLonLat(c)));
492
- geometry = new Polygon(rings);
493
- }
494
- else {
495
- geometry = new Point([0, 0]);
496
- }
497
- const olFeature = new OLFeature({
498
- geometry,
499
- ...feature.properties,
500
- });
501
- olFeature.setId(feature.id);
502
- return olFeature;
503
- });
504
- vectorSource.addFeatures(olFeatures);
505
- }
506
- const layer = new HeatmapLayer({
507
- source: vectorSource,
508
- visible: config.visible ?? true,
509
- opacity: config.opacity ?? 1,
510
- zIndex: config.zIndex,
511
- ...(config.blur !== undefined && { blur: config.blur }),
512
- ...(config.radius !== undefined && { radius: config.radius }),
513
- ...(config.weight !== undefined && {
514
- weight: config.weight,
515
- }),
516
- });
517
- layer.set('id', config.id);
518
- map.addLayer(layer);
519
- this.layerCache.set(config.id, layer);
520
- this.updateLayerState();
521
- return { id: config.id };
522
- }
523
- createTileLayer(config, map) {
524
- let source;
525
- switch (config.source.type) {
526
- case 'osm':
527
- source = new OSM({ attributions: config.source.attributions });
528
- break;
529
- case 'xyz':
530
- source = new XYZ({ url: config.source.url, attributions: config.source.attributions });
531
- break;
532
- case 'wms':
533
- source = new TileWMS({
534
- url: config.source.url,
535
- params: config.source.params ?? {},
536
- attributions: config.source.attributions,
537
- });
538
- break;
539
- default:
540
- source = new OSM();
541
- }
542
- const layer = new TileLayer({
543
- source,
544
- visible: config.visible ?? true,
545
- opacity: config.opacity ?? 1,
546
- zIndex: config.zIndex,
547
- });
548
- layer.set('id', config.id);
549
- map.addLayer(layer);
550
- this.layerCache.set(config.id, layer);
551
- this.updateLayerState();
552
- return { id: config.id };
553
- }
554
- createImageLayer(config, map) {
555
- let source;
556
- if (config.source.type === 'static') {
557
- source = new ImageStatic({
558
- url: config.source.url,
559
- imageExtent: config.source.imageExtent ?? [0, 0, 1, 1],
560
- });
561
- }
562
- else {
563
- source = new ImageWMS({ url: config.source.url, params: config.source.params });
564
- }
565
- const layer = new ImageLayer({
566
- source,
567
- visible: config.visible ?? true,
568
- opacity: config.opacity ?? 1,
569
- zIndex: config.zIndex,
570
- });
571
- layer.set('id', config.id);
572
- map.addLayer(layer);
573
- this.layerCache.set(config.id, layer);
574
- this.updateLayerState();
575
- return { id: config.id };
576
- }
577
675
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlLayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
578
676
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlLayerService });
579
677
  }
@@ -586,8 +684,10 @@ class OlClusterComponent {
586
684
  minDistance = input(20, ...(ngDevMode ? [{ debugName: "minDistance" }] : /* istanbul ignore next */ []));
587
685
  showCount = input(true, ...(ngDevMode ? [{ debugName: "showCount" }] : /* istanbul ignore next */ []));
588
686
  featureStyle = input(...(ngDevMode ? [undefined, { debugName: "featureStyle" }] : /* istanbul ignore next */ []));
687
+ spiderfyOnSelect = input(false, ...(ngDevMode ? [{ debugName: "spiderfyOnSelect" }] : /* istanbul ignore next */ []));
688
+ spiderfyClick = output();
589
689
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlClusterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
590
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: OlClusterComponent, isStandalone: true, selector: "ol-cluster", inputs: { distance: { classPropertyName: "distance", publicName: "distance", isSignal: true, isRequired: false, transformFunction: null }, minDistance: { classPropertyName: "minDistance", publicName: "minDistance", isSignal: true, isRequired: false, transformFunction: null }, showCount: { classPropertyName: "showCount", publicName: "showCount", isSignal: true, isRequired: false, transformFunction: null }, featureStyle: { classPropertyName: "featureStyle", publicName: "featureStyle", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
690
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: OlClusterComponent, isStandalone: true, selector: "ol-cluster", inputs: { distance: { classPropertyName: "distance", publicName: "distance", isSignal: true, isRequired: false, transformFunction: null }, minDistance: { classPropertyName: "minDistance", publicName: "minDistance", isSignal: true, isRequired: false, transformFunction: null }, showCount: { classPropertyName: "showCount", publicName: "showCount", isSignal: true, isRequired: false, transformFunction: null }, featureStyle: { classPropertyName: "featureStyle", publicName: "featureStyle", isSignal: true, isRequired: false, transformFunction: null }, spiderfyOnSelect: { classPropertyName: "spiderfyOnSelect", publicName: "spiderfyOnSelect", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { spiderfyClick: "spiderfyClick" }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
591
691
  }
592
692
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlClusterComponent, decorators: [{
593
693
  type: Component,
@@ -596,7 +696,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
596
696
  template: '',
597
697
  changeDetection: ChangeDetectionStrategy.OnPush,
598
698
  }]
599
- }], propDecorators: { distance: [{ type: i0.Input, args: [{ isSignal: true, alias: "distance", required: false }] }], minDistance: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDistance", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], featureStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "featureStyle", required: false }] }] } });
699
+ }], propDecorators: { distance: [{ type: i0.Input, args: [{ isSignal: true, alias: "distance", required: false }] }], minDistance: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDistance", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], featureStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "featureStyle", required: false }] }], spiderfyOnSelect: [{ type: i0.Input, args: [{ isSignal: true, alias: "spiderfyOnSelect", required: false }] }], spiderfyClick: [{ type: i0.Output, args: ["spiderfyClick"] }] } });
600
700
 
601
701
  // OlVectorLayerComponent
602
702
  class OlVectorLayerComponent {
@@ -612,6 +712,7 @@ class OlVectorLayerComponent {
612
712
  style = input(...(ngDevMode ? [undefined, { debugName: "style" }] : /* istanbul ignore next */ []));
613
713
  cluster = input(...(ngDevMode ? [undefined, { debugName: "cluster" }] : /* istanbul ignore next */ []));
614
714
  clusterComponent = contentChild(OlClusterComponent, ...(ngDevMode ? [{ debugName: "clusterComponent" }] : /* istanbul ignore next */ []));
715
+ coordinateProjection = input('EPSG:4326', ...(ngDevMode ? [{ debugName: "coordinateProjection" }] : /* istanbul ignore next */ []));
615
716
  constructor() {
616
717
  // Initialize layer after DOM is ready
617
718
  afterNextRender(() => {
@@ -624,6 +725,8 @@ class OlVectorLayerComponent {
624
725
  minDistance: clusterCmp.minDistance(),
625
726
  showCount: clusterCmp.showCount(),
626
727
  featureStyle: clusterCmp.featureStyle(),
728
+ spiderfyOnSelect: clusterCmp.spiderfyOnSelect(),
729
+ onSpiderfyClick: (f) => clusterCmp.spiderfyClick.emit(f),
627
730
  }
628
731
  : undefined);
629
732
  this.layerService.addLayer({
@@ -637,6 +740,7 @@ class OlVectorLayerComponent {
637
740
  visible: this.visible(),
638
741
  style: this.style(),
639
742
  cluster: resolvedClusterConfig,
743
+ coordinateProjection: this.coordinateProjection(),
640
744
  });
641
745
  });
642
746
  // Effect to sync features when input changes
@@ -656,13 +760,24 @@ class OlVectorLayerComponent {
656
760
  effect(() => {
657
761
  this.layerService.setZIndex(this.id(), this.zIndex());
658
762
  });
763
+ // Reactive cluster distance updates
764
+ effect(() => {
765
+ const clusterCmp = this.clusterComponent();
766
+ if (clusterCmp) {
767
+ const dist = clusterCmp.distance();
768
+ const minDst = clusterCmp.minDistance();
769
+ // Since we are inside effect, these will trigger when distance changes
770
+ this.layerService.setClusterDistance(this.id(), dist);
771
+ this.layerService.setClusterMinDistance(this.id(), minDst);
772
+ }
773
+ });
659
774
  // Cleanup when component is destroyed
660
775
  this.destroyRef.onDestroy(() => {
661
776
  this.layerService.removeLayer(this.id());
662
777
  });
663
778
  }
664
779
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlVectorLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
665
- 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 });
666
781
  }
667
782
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlVectorLayerComponent, decorators: [{
668
783
  type: Component,
@@ -671,7 +786,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
671
786
  template: '',
672
787
  changeDetection: ChangeDetectionStrategy.OnPush,
673
788
  }]
674
- }], 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 }] }] } });
675
790
 
676
791
  // OlTileLayerComponent
677
792
  class OlTileLayerComponent {
@@ -963,6 +1078,16 @@ class OlWebGLVectorLayerComponent {
963
1078
  }
964
1079
  });
965
1080
  }
1081
+ /**
1082
+ * Imperatively update style variables without triggering Angular change detection.
1083
+ * Ideal for 60FPS animations (e.g., linked to OlTimeService) where you don't want
1084
+ * to use the declarative [variables] input.
1085
+ */
1086
+ updateVariables(vars) {
1087
+ if (this.layer) {
1088
+ this.layer.updateStyleVariables(vars);
1089
+ }
1090
+ }
966
1091
  syncFeatures(features) {
967
1092
  this.vectorSource.clear();
968
1093
  if (!features?.length)
@@ -984,7 +1109,7 @@ class OlWebGLVectorLayerComponent {
984
1109
  }
985
1110
  else if (geom.type === 'Circle') {
986
1111
  const center = fromLonLat(geom.coordinates);
987
- geometry = new Circle(center, geom.radius ?? 1000);
1112
+ geometry = new Circle$1(center, geom.radius ?? 1000);
988
1113
  }
989
1114
  else {
990
1115
  geometry = new Point([0, 0]);