@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,35 +1,357 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, signal, computed, Injectable, DestroyRef, input, afterNextRender, effect, ChangeDetectionStrategy, Component } 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
+ import HeatmapLayer from 'ol/layer/Heatmap';
4
5
  import TileLayer from 'ol/layer/Tile';
5
6
  import ImageLayer from 'ol/layer/Image';
6
7
  import VectorSource from 'ol/source/Vector';
7
- import OLFeature from 'ol/Feature';
8
- import { Point, LineString, Polygon, Circle } from 'ol/geom';
9
- import { fromLonLat } from 'ol/proj';
10
- import { getCenter } from 'ol/extent';
11
- import { Style, Circle as Circle$1, Stroke, Fill, Icon } from 'ol/style';
12
- import Text from 'ol/style/Text';
13
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';
14
15
  import OSM from 'ol/source/OSM';
15
16
  import XYZ from 'ol/source/XYZ';
16
17
  import TileWMS from 'ol/source/TileWMS';
17
18
  import ImageWMS from 'ol/source/ImageWMS';
18
19
  import ImageStatic from 'ol/source/ImageStatic';
19
- 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';
23
+ import WebGLVectorLayer from 'ol/layer/WebGLVector';
24
+ import WebGLTileLayer from 'ol/layer/WebGLTile';
25
+ import WebGLVectorTileLayer from 'ol/layer/WebGLVectorTile';
26
+ import VectorTileSource from 'ol/source/VectorTile';
27
+ import MVT from 'ol/format/MVT';
28
+
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
+ return layer;
170
+ }
171
+ function buildHeatmapLayer(config) {
172
+ const vectorSource = new VectorSource();
173
+ if (config.features && config.features.length > 0) {
174
+ const olFeatures = config.features.map(featureToOlFeature);
175
+ vectorSource.addFeatures(olFeatures);
176
+ }
177
+ const layer = new HeatmapLayer({
178
+ source: vectorSource,
179
+ visible: config.visible ?? true,
180
+ opacity: config.opacity ?? 1,
181
+ zIndex: config.zIndex,
182
+ ...(config.blur !== undefined && { blur: config.blur }),
183
+ ...(config.radius !== undefined && { radius: config.radius }),
184
+ ...(config.weight !== undefined && {
185
+ weight: config.weight,
186
+ }),
187
+ });
188
+ layer.set('id', config.id);
189
+ return layer;
190
+ }
191
+ function buildTileLayer(config, source) {
192
+ const layer = new TileLayer({
193
+ source,
194
+ visible: config.visible ?? true,
195
+ opacity: config.opacity ?? 1,
196
+ zIndex: config.zIndex,
197
+ });
198
+ layer.set('id', config.id);
199
+ return layer;
200
+ }
201
+ function buildImageLayer(config, source) {
202
+ const layer = new ImageLayer({
203
+ source,
204
+ visible: config.visible ?? true,
205
+ opacity: config.opacity ?? 1,
206
+ zIndex: config.zIndex,
207
+ });
208
+ layer.set('id', config.id);
209
+ return layer;
210
+ }
211
+
212
+ class SpiderficationManager {
213
+ spiderSource = new VectorSource();
214
+ spiderLayer = new VectorLayer({
215
+ source: this.spiderSource,
216
+ zIndex: 9999,
217
+ properties: { 'is-spider-layer': true },
218
+ });
219
+ mapClickListenerRegistered = false;
220
+ map = null;
221
+ layerCache;
222
+ constructor(layerCache) {
223
+ this.layerCache = layerCache;
224
+ }
225
+ register(map) {
226
+ if (this.mapClickListenerRegistered)
227
+ return;
228
+ this.mapClickListenerRegistered = true;
229
+ this.map = map;
230
+ map.addLayer(this.spiderLayer);
231
+ // Unspiderfy on map movement or zoom
232
+ map.on('movestart', () => this.unspiderfy());
233
+ map.on('singleclick', (e) => {
234
+ let handled = false;
235
+ let keepSpiderOpen = false;
236
+ map.forEachFeatureAtPixel(e.pixel, (f, l) => {
237
+ if (handled)
238
+ return;
239
+ // Check if we clicked a spider item
240
+ if (l === this.spiderLayer) {
241
+ const originalOlFeature = f.get('spider-feature');
242
+ if (originalOlFeature) {
243
+ const layerId = f.get('cluster-layer-id');
244
+ const layerObj = this.layerCache.get(layerId);
245
+ if (layerObj) {
246
+ const clusterCfg = layerObj.get('cluster-config');
247
+ if (clusterCfg?.onSpiderfyClick) {
248
+ // Use olFeatureToFeature so coordinates are properly extracted!
249
+ const feat = olFeatureToFeature(originalOlFeature);
250
+ clusterCfg.onSpiderfyClick(feat);
251
+ }
252
+ }
253
+ }
254
+ handled = true;
255
+ keepSpiderOpen = true; // Keep spider open when clicking a leg
256
+ return;
257
+ }
258
+ // Check if we clicked a cluster
259
+ if (!l)
260
+ return;
261
+ const features = f.get('features');
262
+ if (features && features.length > 1) {
263
+ const clusterCfg = l.get('cluster-config');
264
+ if (clusterCfg?.spiderfyOnSelect) {
265
+ keepSpiderOpen = true;
266
+ handled = true;
267
+ // Execute layer manipulations outside the synchronous event loop
268
+ setTimeout(() => {
269
+ this.spiderfy(map, f, features, l, clusterCfg);
270
+ });
271
+ }
272
+ }
273
+ });
274
+ // Cleanup existing spider layer if we clicked anything else
275
+ if (!keepSpiderOpen) {
276
+ this.unspiderfy();
277
+ }
278
+ });
279
+ }
280
+ unspiderfy() {
281
+ this.spiderSource.clear();
282
+ }
283
+ spiderfy(map, clusterFeature, features, parentLayer, cfg) {
284
+ this.unspiderfy();
285
+ const count = features.length;
286
+ const centerGeom = clusterFeature.getGeometry();
287
+ if (!centerGeom || centerGeom.getType() !== 'Point')
288
+ return;
289
+ const centerCoords = centerGeom.getCoordinates();
290
+ const resolution = map.getView().getResolution() ?? 1;
291
+ const baseRadius = 30; // 30 pixels
292
+ const radius = baseRadius + count * 2;
293
+ const angleStep = (2 * Math.PI) / count;
294
+ const spiderFeatures = [];
295
+ const styleFn = parentLayer.get('style-fn');
296
+ features.forEach((f, i) => {
297
+ let x, y;
298
+ if (count <= 8) {
299
+ // Circle layout for small number of features
300
+ const angle = i * angleStep;
301
+ x = centerCoords[0] + radius * Math.cos(angle) * resolution;
302
+ y = centerCoords[1] + radius * Math.sin(angle) * resolution;
303
+ }
304
+ else {
305
+ // Spiral layout (caracol) for many features
306
+ const initialRadius = 20;
307
+ const legLength = 15;
308
+ const spiralAngleStep = 0.5;
309
+ const angle = i * spiralAngleStep;
310
+ const r = initialRadius + legLength * (angle / Math.PI);
311
+ x = centerCoords[0] + r * Math.cos(angle) * resolution;
312
+ y = centerCoords[1] + r * Math.sin(angle) * resolution;
313
+ }
314
+ const legGeom = new Point([x, y]);
315
+ // Create leg line
316
+ const lineFeature = new OLFeature(new LineString([centerCoords, [x, y]]));
317
+ lineFeature.setStyle(new Style({ stroke: new Stroke({ color: 'rgba(0,0,0,0.5)', width: 2 }) }));
318
+ spiderFeatures.push(lineFeature);
319
+ // Create point feature
320
+ const pointFeature = new OLFeature(legGeom);
321
+ // Determine style for the spider leg point
322
+ let pointStyle = undefined;
323
+ if (cfg?.featureStyle) {
324
+ pointStyle = cfg.featureStyle;
325
+ }
326
+ else if (styleFn) {
327
+ pointStyle = styleFn(f, resolution);
328
+ }
329
+ // Ensure we always have a visible style even if the original feature has none
330
+ if (!pointStyle || (Array.isArray(pointStyle) && pointStyle.length === 0)) {
331
+ pointStyle = new Style({
332
+ image: new Circle({
333
+ radius: 6,
334
+ fill: new Fill({ color: '#3399CC' }),
335
+ stroke: new Stroke({ color: '#fff', width: 2 }),
336
+ }),
337
+ });
338
+ }
339
+ pointFeature.setStyle(pointStyle);
340
+ pointFeature.set('spider-feature', f);
341
+ pointFeature.set('cluster-layer-id', parentLayer.get('id'));
342
+ spiderFeatures.push(pointFeature);
343
+ });
344
+ this.spiderSource.addFeatures(spiderFeatures);
345
+ }
346
+ }
20
347
 
21
- // OlLayerService
22
- /**
23
- * Internal property key used to stash the abstract style metadata on the
24
- * underlying `ol/Feature` so the layer style function can resolve a
25
- * per-feature visual without colliding with user `properties`.
26
- */
27
348
  const STYLE_PROP = '__angular_helpers_style__';
28
349
  class OlLayerService {
29
350
  mapService = inject(OlMapService);
30
351
  layerCache = new Map();
31
352
  pendingConfigs = [];
32
353
  layerState = signal([], ...(ngDevMode ? [{ debugName: "layerState" }] : /* istanbul ignore next */ []));
354
+ spiderManager = new SpiderficationManager(this.layerCache);
33
355
  layers = computed(() => this.layerState(), ...(ngDevMode ? [{ debugName: "layers" }] : /* istanbul ignore next */ []));
34
356
  visibleLayers = computed(() => this.layerState().filter((l) => l.visible), ...(ngDevMode ? [{ debugName: "visibleLayers" }] : /* istanbul ignore next */ []));
35
357
  tileLayers = computed(() => this.layerState().filter((l) => l.type === 'tile'), ...(ngDevMode ? [{ debugName: "tileLayers" }] : /* istanbul ignore next */ []));
@@ -55,16 +377,74 @@ class OlLayerService {
55
377
  }
56
378
  }
57
379
  createLayer(config, map) {
380
+ let layer;
58
381
  switch (config.type) {
59
- case 'vector':
60
- return this.createVectorLayer(config, map);
61
- case 'tile':
62
- return this.createTileLayer(config, map);
63
- case 'image':
64
- return this.createImageLayer(config, map);
382
+ case 'vector': {
383
+ const vConfig = config;
384
+ const sourceOptions = {};
385
+ if (vConfig.url && vConfig.format) {
386
+ sourceOptions.url = vConfig.url;
387
+ if (vConfig.format === 'geojson')
388
+ sourceOptions.format = new GeoJSON();
389
+ else if (vConfig.format === 'topojson')
390
+ sourceOptions.format = new TopoJSON();
391
+ else if (vConfig.format === 'kml')
392
+ sourceOptions.format = new KML();
393
+ }
394
+ const vectorSource = new VectorSource(sourceOptions);
395
+ if (vConfig.features && vConfig.features.length > 0) {
396
+ const olFeatures = vConfig.features.map((f) => {
397
+ const olf = featureToOlFeature(f);
398
+ if (f.style)
399
+ olf.set(STYLE_PROP, f.style);
400
+ return olf;
401
+ });
402
+ vectorSource.addFeatures(olFeatures);
403
+ }
404
+ const clusterCfg = vConfig.cluster;
405
+ const source = clusterCfg?.enabled
406
+ ? new ClusterSource({
407
+ source: vectorSource,
408
+ distance: clusterCfg.distance ?? 40,
409
+ minDistance: clusterCfg.minDistance ?? 20,
410
+ geometryFunction: (feature) => {
411
+ const geometry = feature.getGeometry();
412
+ if (!geometry)
413
+ return null;
414
+ if (geometry.getType() === 'Point')
415
+ return geometry;
416
+ return new Point(getCenter(geometry.getExtent()));
417
+ },
418
+ })
419
+ : vectorSource;
420
+ layer = buildVectorLayer(vConfig, source);
421
+ if (clusterCfg?.spiderfyOnSelect) {
422
+ this.spiderManager.register(map);
423
+ }
424
+ break;
425
+ }
426
+ case 'heatmap':
427
+ layer = buildHeatmapLayer(config);
428
+ break;
429
+ case 'tile': {
430
+ const tConfig = config;
431
+ const source = buildTileSource(tConfig.source);
432
+ layer = buildTileLayer(tConfig, source);
433
+ break;
434
+ }
435
+ case 'image': {
436
+ const iConfig = config;
437
+ const source = buildImageSource(iConfig.source);
438
+ layer = buildImageLayer(iConfig, source);
439
+ break;
440
+ }
65
441
  default:
66
442
  return { id: config.id };
67
443
  }
444
+ map.addLayer(layer);
445
+ this.layerCache.set(config.id, layer);
446
+ this.updateLayerState();
447
+ return { id: config.id };
68
448
  }
69
449
  getLayer(id) {
70
450
  return this.layerCache.get(id);
@@ -73,7 +453,6 @@ class OlLayerService {
73
453
  return this.layerCache.has(id);
74
454
  }
75
455
  removeLayer(id) {
76
- // Cancel if it's still pending (map not ready yet)
77
456
  const pendingIdx = this.pendingConfigs.findIndex((c) => c.id === id);
78
457
  if (pendingIdx !== -1) {
79
458
  this.pendingConfigs.splice(pendingIdx, 1);
@@ -83,6 +462,7 @@ class OlLayerService {
83
462
  const layer = this.layerCache.get(id);
84
463
  if (map && layer) {
85
464
  map.removeLayer(layer);
465
+ layer.dispose();
86
466
  this.layerCache.delete(id);
87
467
  this.updateLayerState();
88
468
  }
@@ -93,6 +473,11 @@ class OlLayerService {
93
473
  layer.setVisible(visible);
94
474
  this.updateLayerState();
95
475
  }
476
+ else {
477
+ const pending = this.pendingConfigs.find((c) => c.id === id);
478
+ if (pending)
479
+ pending.visible = visible;
480
+ }
96
481
  }
97
482
  toggleVisibility(id) {
98
483
  const layer = this.layerCache.get(id);
@@ -110,6 +495,11 @@ class OlLayerService {
110
495
  layer.setOpacity(opacity);
111
496
  this.updateLayerState();
112
497
  }
498
+ else {
499
+ const pending = this.pendingConfigs.find((c) => c.id === id);
500
+ if (pending)
501
+ pending.opacity = opacity;
502
+ }
113
503
  }
114
504
  setZIndex(id, zIndex) {
115
505
  const layer = this.layerCache.get(id);
@@ -117,6 +507,52 @@ class OlLayerService {
117
507
  layer.setZIndex(zIndex);
118
508
  this.updateLayerState();
119
509
  }
510
+ else {
511
+ const pending = this.pendingConfigs.find((c) => c.id === id);
512
+ if (pending)
513
+ pending.zIndex = zIndex;
514
+ }
515
+ }
516
+ setClusterDistance(id, distance) {
517
+ const layer = this.layerCache.get(id);
518
+ if (layer instanceof VectorLayer) {
519
+ const source = layer.getSource();
520
+ if (source && 'setDistance' in source) {
521
+ source.setDistance(distance);
522
+ }
523
+ }
524
+ }
525
+ setClusterMinDistance(id, minDistance) {
526
+ const layer = this.layerCache.get(id);
527
+ if (layer instanceof VectorLayer) {
528
+ const source = layer.getSource();
529
+ if (source && 'setMinDistance' in source) {
530
+ source.setMinDistance(minDistance);
531
+ }
532
+ }
533
+ }
534
+ setHeatmapProperties(id, props) {
535
+ const layer = this.layerCache.get(id);
536
+ if (layer instanceof HeatmapLayer) {
537
+ if (props.blur !== undefined)
538
+ layer.setBlur(props.blur);
539
+ if (props.radius !== undefined)
540
+ layer.setRadius(props.radius);
541
+ if (props.weight !== undefined)
542
+ layer.setWeight(props.weight);
543
+ }
544
+ else {
545
+ const pending = this.pendingConfigs.find((c) => c.id === id);
546
+ if (pending && pending.type === 'heatmap') {
547
+ const heatmapConfig = pending;
548
+ if (props.blur !== undefined)
549
+ heatmapConfig.blur = props.blur;
550
+ if (props.radius !== undefined)
551
+ heatmapConfig.radius = props.radius;
552
+ if (props.weight !== undefined)
553
+ heatmapConfig.weight = props.weight;
554
+ }
555
+ }
120
556
  }
121
557
  isVisible(id) {
122
558
  return this.layerCache.get(id)?.getVisible() ?? false;
@@ -127,11 +563,6 @@ class OlLayerService {
127
563
  getZIndex(id) {
128
564
  return this.layerCache.get(id)?.getZIndex() ?? 0;
129
565
  }
130
- /**
131
- * Clears all features from a vector layer's source.
132
- * Does not remove the layer itself.
133
- * @param id - Layer identifier
134
- */
135
566
  clearFeatures(id) {
136
567
  const layer = this.layerCache.get(id);
137
568
  if (!(layer instanceof VectorLayer))
@@ -139,19 +570,12 @@ class OlLayerService {
139
570
  const source = layer.getSource();
140
571
  if (!source)
141
572
  return;
142
- // Handle Cluster source: clear the underlying VectorSource
143
573
  const clusterSource = source;
144
574
  const vectorSource = clusterSource.getSource
145
575
  ? clusterSource.getSource()
146
576
  : source;
147
577
  vectorSource?.clear();
148
578
  }
149
- /**
150
- * Updates the features of a vector layer.
151
- * Syncs new features without clearing existing ones (preserves OL modifications).
152
- * @param id - Layer identifier
153
- * @param features - New features to sync
154
- */
155
579
  updateFeatures(id, features) {
156
580
  const layer = this.layerCache.get(id);
157
581
  if (!(layer instanceof VectorLayer))
@@ -159,58 +583,32 @@ class OlLayerService {
159
583
  const source = layer.getSource();
160
584
  if (!source)
161
585
  return;
162
- // Handle Cluster source: get the underlying VectorSource
163
586
  const clusterSource = source;
164
587
  const vectorSource = clusterSource.getSource
165
588
  ? clusterSource.getSource()
166
589
  : source;
167
590
  if (!(vectorSource instanceof VectorSource))
168
591
  return;
169
- // Get existing feature IDs from source
170
- const existingIds = new Set(vectorSource
171
- .getFeatures()
172
- .map((f) => f.getId())
173
- .filter((id) => id !== undefined));
174
- // Only add features that don't already exist in the source
175
- if (features && features.length > 0) {
176
- const newFeatures = features.filter((f) => !existingIds.has(f.id));
177
- if (newFeatures.length > 0) {
178
- const olFeatures = newFeatures.map((feature) => {
179
- const geom = feature.geometry;
180
- let geometry;
181
- // Validate coordinates exist before processing
182
- if (!geom.coordinates) {
183
- geometry = new Point([0, 0]);
184
- }
185
- else if (geom.type === 'Point') {
186
- const coords = geom.coordinates;
187
- geometry = new Point(fromLonLat(coords));
188
- }
189
- else if (geom.type === 'LineString') {
190
- const coords = geom.coordinates.map((c) => fromLonLat(c));
191
- geometry = new LineString(coords);
192
- }
193
- else if (geom.type === 'Polygon') {
194
- const rings = geom.coordinates.map((ring) => ring.map((c) => fromLonLat(c)));
195
- geometry = new Polygon(rings);
196
- }
197
- else if (geom.type === 'Circle') {
198
- const center = fromLonLat(geom.coordinates);
199
- // Approximate radius in meters - use 1000m as default if not specified
200
- geometry = new Circle(center, geom.radius ?? 1000);
201
- }
202
- else {
203
- geometry = new Point([0, 0]);
204
- }
205
- const olFeature = new OLFeature({
206
- geometry,
207
- ...feature.properties,
208
- });
209
- if (feature.style) {
210
- olFeature.set(STYLE_PROP, feature.style);
211
- }
212
- olFeature.setId(feature.id);
213
- return olFeature;
592
+ if (features) {
593
+ const newFeatureIds = new Set(features.map((f) => f.id));
594
+ const sourceFeatures = vectorSource.getFeatures();
595
+ sourceFeatures.forEach((f) => {
596
+ const fId = f.getId();
597
+ if (fId !== undefined && !newFeatureIds.has(fId)) {
598
+ vectorSource.removeFeature(f);
599
+ }
600
+ });
601
+ const existingIds = new Set(vectorSource
602
+ .getFeatures()
603
+ .map((f) => f.getId())
604
+ .filter((fId) => fId !== undefined));
605
+ const featuresToAdd = features.filter((f) => !existingIds.has(f.id));
606
+ if (featuresToAdd.length > 0) {
607
+ const olFeatures = featuresToAdd.map((f) => {
608
+ const olf = featureToOlFeature(f);
609
+ if (f.style)
610
+ olf.set(STYLE_PROP, f.style);
611
+ return olf;
214
612
  });
215
613
  vectorSource.addFeatures(olFeatures);
216
614
  }
@@ -219,7 +617,13 @@ class OlLayerService {
219
617
  updateLayerState() {
220
618
  const layers = [];
221
619
  this.layerCache.forEach((layer, id) => {
222
- const type = layer instanceof VectorLayer ? 'vector' : layer instanceof TileLayer ? 'tile' : 'image';
620
+ let type = 'vector';
621
+ if (layer instanceof HeatmapLayer)
622
+ type = 'heatmap';
623
+ else if (layer instanceof TileLayer)
624
+ type = 'tile';
625
+ else if (layer instanceof ImageLayer)
626
+ type = 'image';
223
627
  layers.push({
224
628
  id,
225
629
  type: type,
@@ -230,252 +634,73 @@ class OlLayerService {
230
634
  });
231
635
  this.layerState.set(layers.sort((a, b) => a.zIndex - b.zIndex));
232
636
  }
233
- createVectorLayer(config, map) {
234
- const vectorSource = new VectorSource();
235
- // Add features if provided
236
- if (config.features && config.features.length > 0) {
237
- const olFeatures = config.features.map((feature) => {
238
- const geom = feature.geometry;
239
- let geometry;
240
- // Validate coordinates exist before processing
241
- if (!geom.coordinates) {
242
- geometry = new Point([0, 0]);
243
- }
244
- else if (geom.type === 'Point') {
245
- // Transform from EPSG:4326 (lon/lat) to EPSG:3857 (map projection)
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,
267
- });
268
- if (feature.style) {
269
- olFeature.set(STYLE_PROP, feature.style);
270
- }
271
- olFeature.setId(feature.id);
272
- return olFeature;
273
- });
274
- vectorSource.addFeatures(olFeatures);
275
- }
276
- // Wrap in cluster source if enabled
277
- const clusterCfg = config.cluster;
278
- const source = clusterCfg?.enabled
279
- ? new ClusterSource({
280
- source: vectorSource,
281
- distance: clusterCfg.distance ?? 40,
282
- minDistance: clusterCfg.minDistance ?? 20,
283
- geometryFunction: (feature) => {
284
- const geometry = feature.getGeometry();
285
- if (!geometry)
286
- return null;
287
- // For Point geometries, use as-is
288
- if (geometry.getType() === 'Point') {
289
- return geometry;
290
- }
291
- // For other geometries (Polygon, Circle, etc.), use center point
292
- const extent = geometry.getExtent();
293
- const center = getCenter(extent);
294
- return new Point(center);
295
- },
296
- })
297
- : vectorSource;
298
- // Default style for all geometry types (points, lines, polygons)
299
- const defaultStyle = new Style({
300
- fill: new Fill({ color: 'rgba(25, 118, 210, 0.3)' }),
301
- stroke: new Stroke({ color: '#1976d2', width: 2 }),
302
- image: new Circle$1({
303
- radius: 8,
304
- fill: new Fill({ color: '#1976d2' }),
305
- stroke: new Stroke({ color: '#d32f2f', width: 2 }),
306
- }),
307
- });
308
- // Cluster style: shows count badge when features are clustered
309
- const clusterStyleFn = (olFeature) => {
310
- const features = olFeature.get('features');
311
- const size = features?.length ?? 1;
312
- if (size > 1) {
313
- const showCount = clusterCfg?.showCount ?? true;
314
- return new Style({
315
- image: new Circle$1({
316
- radius: 15 + Math.min(size * 2, 15),
317
- fill: new Fill({ color: 'rgba(255, 100, 100, 0.8)' }),
318
- stroke: new Stroke({ color: '#fff', width: 2 }),
319
- }),
320
- text: showCount
321
- ? new Text({
322
- text: String(size),
323
- fill: new Fill({ color: '#fff' }),
324
- })
325
- : undefined,
326
- });
327
- }
328
- // Single feature: get the original feature from the cluster and use its style
329
- const originalFeatures = olFeature.get('features');
330
- const originalFeature = originalFeatures?.[0];
331
- if (originalFeature) {
332
- const abstractStyle = originalFeature.get(STYLE_PROP);
333
- if (abstractStyle) {
334
- const style = new Style();
335
- const { icon, fill, stroke } = abstractStyle;
336
- if (icon?.src) {
337
- style.setImage(new Icon({
338
- src: icon.src,
339
- ...(icon.size ? { size: icon.size } : {}),
340
- ...(icon.anchor ? { anchor: icon.anchor } : {}),
341
- }));
342
- }
343
- if (fill) {
344
- style.setFill(new Fill({ color: fill.color }));
345
- }
346
- if (stroke) {
347
- style.setStroke(new Stroke({ color: stroke.color, width: stroke.width }));
348
- }
349
- // If we mapped at least one property, return it, otherwise fallback
350
- if (icon?.src || fill || stroke)
351
- return style;
352
- }
353
- }
354
- return defaultStyle;
355
- };
356
- // Per-feature style resolver: features carrying `style` render it.
357
- // Structural type avoids importing `FeatureLike` from `ol/Feature`;
358
- // tooling has been observed to auto-remove the unused-looking import.
359
- const styleFn = (olFeature) => {
360
- const abstractStyle = olFeature.get(STYLE_PROP);
361
- if (abstractStyle) {
362
- const style = new Style();
363
- const { icon, fill, stroke } = abstractStyle;
364
- if (icon?.src) {
365
- style.setImage(new Icon({
366
- src: icon.src,
367
- ...(icon.size ? { size: icon.size } : {}),
368
- ...(icon.anchor ? { anchor: icon.anchor } : {}),
369
- }));
370
- }
371
- if (fill) {
372
- style.setFill(new Fill({ color: fill.color }));
373
- }
374
- if (stroke) {
375
- style.setStroke(new Stroke({ color: stroke.color, width: stroke.width }));
376
- }
377
- if (icon?.src || fill || stroke)
378
- return style;
379
- }
380
- return defaultStyle;
381
- };
382
- const layer = new VectorLayer({
383
- source,
384
- visible: config.visible ?? true,
385
- opacity: config.opacity ?? 1,
386
- zIndex: config.zIndex,
387
- style: clusterCfg?.enabled ? clusterStyleFn : styleFn,
388
- });
389
- layer.set('id', config.id);
390
- map.addLayer(layer);
391
- this.layerCache.set(config.id, layer);
392
- this.updateLayerState();
393
- return { id: config.id };
394
- }
395
- createTileLayer(config, map) {
396
- let source;
397
- switch (config.source.type) {
398
- case 'osm':
399
- source = new OSM({ attributions: config.source.attributions });
400
- break;
401
- case 'xyz':
402
- source = new XYZ({ url: config.source.url, attributions: config.source.attributions });
403
- break;
404
- case 'wms':
405
- source = new TileWMS({
406
- url: config.source.url,
407
- params: config.source.params,
408
- attributions: config.source.attributions,
409
- });
410
- break;
411
- default:
412
- source = new OSM();
413
- }
414
- const layer = new TileLayer({
415
- source,
416
- visible: config.visible ?? true,
417
- opacity: config.opacity ?? 1,
418
- zIndex: config.zIndex,
419
- });
420
- layer.set('id', config.id);
421
- map.addLayer(layer);
422
- this.layerCache.set(config.id, layer);
423
- this.updateLayerState();
424
- return { id: config.id };
425
- }
426
- createImageLayer(config, map) {
427
- let source;
428
- if (config.source.type === 'static') {
429
- source = new ImageStatic({
430
- url: config.source.url,
431
- imageExtent: config.source.imageExtent ?? [0, 0, 1, 1],
432
- });
433
- }
434
- else {
435
- source = new ImageWMS({ url: config.source.url, params: config.source.params });
436
- }
437
- const layer = new ImageLayer({
438
- source,
439
- visible: config.visible ?? true,
440
- opacity: config.opacity ?? 1,
441
- zIndex: config.zIndex,
442
- });
443
- layer.set('id', config.id);
444
- map.addLayer(layer);
445
- this.layerCache.set(config.id, layer);
446
- this.updateLayerState();
447
- return { id: config.id };
448
- }
449
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlLayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
450
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlLayerService });
637
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlLayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
638
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlLayerService });
451
639
  }
452
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlLayerService, decorators: [{
640
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlLayerService, decorators: [{
453
641
  type: Injectable
454
642
  }] });
455
643
 
644
+ class OlClusterComponent {
645
+ distance = input(40, ...(ngDevMode ? [{ debugName: "distance" }] : /* istanbul ignore next */ []));
646
+ minDistance = input(20, ...(ngDevMode ? [{ debugName: "minDistance" }] : /* istanbul ignore next */ []));
647
+ showCount = input(true, ...(ngDevMode ? [{ debugName: "showCount" }] : /* istanbul ignore next */ []));
648
+ featureStyle = input(...(ngDevMode ? [undefined, { debugName: "featureStyle" }] : /* istanbul ignore next */ []));
649
+ spiderfyOnSelect = input(false, ...(ngDevMode ? [{ debugName: "spiderfyOnSelect" }] : /* istanbul ignore next */ []));
650
+ spiderfyClick = output();
651
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlClusterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
652
+ 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 });
653
+ }
654
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlClusterComponent, decorators: [{
655
+ type: Component,
656
+ args: [{
657
+ selector: 'ol-cluster',
658
+ template: '',
659
+ changeDetection: ChangeDetectionStrategy.OnPush,
660
+ }]
661
+ }], 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"] }] } });
662
+
456
663
  // OlVectorLayerComponent
457
664
  class OlVectorLayerComponent {
458
665
  layerService = inject(OlLayerService);
459
666
  destroyRef = inject(DestroyRef);
460
667
  id = input.required(...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
461
668
  features = input([], ...(ngDevMode ? [{ debugName: "features" }] : /* istanbul ignore next */ []));
669
+ url = input(...(ngDevMode ? [undefined, { debugName: "url" }] : /* istanbul ignore next */ []));
670
+ format = input(...(ngDevMode ? [undefined, { debugName: "format" }] : /* istanbul ignore next */ []));
462
671
  zIndex = input(0, ...(ngDevMode ? [{ debugName: "zIndex" }] : /* istanbul ignore next */ []));
463
672
  opacity = input(1, ...(ngDevMode ? [{ debugName: "opacity" }] : /* istanbul ignore next */ []));
464
673
  visible = input(true, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
465
674
  style = input(...(ngDevMode ? [undefined, { debugName: "style" }] : /* istanbul ignore next */ []));
466
675
  cluster = input(...(ngDevMode ? [undefined, { debugName: "cluster" }] : /* istanbul ignore next */ []));
676
+ clusterComponent = contentChild(OlClusterComponent, ...(ngDevMode ? [{ debugName: "clusterComponent" }] : /* istanbul ignore next */ []));
467
677
  constructor() {
468
678
  // Initialize layer after DOM is ready
469
679
  afterNextRender(() => {
680
+ const clusterCmp = this.clusterComponent();
681
+ const resolvedClusterConfig = this.cluster() ??
682
+ (clusterCmp
683
+ ? {
684
+ enabled: true,
685
+ distance: clusterCmp.distance(),
686
+ minDistance: clusterCmp.minDistance(),
687
+ showCount: clusterCmp.showCount(),
688
+ featureStyle: clusterCmp.featureStyle(),
689
+ spiderfyOnSelect: clusterCmp.spiderfyOnSelect(),
690
+ onSpiderfyClick: (f) => clusterCmp.spiderfyClick.emit(f),
691
+ }
692
+ : undefined);
470
693
  this.layerService.addLayer({
471
694
  id: this.id(),
472
695
  type: 'vector',
473
696
  features: this.features(),
697
+ url: this.url(),
698
+ format: this.format(),
474
699
  zIndex: this.zIndex(),
475
700
  opacity: this.opacity(),
476
701
  visible: this.visible(),
477
702
  style: this.style(),
478
- cluster: this.cluster(),
703
+ cluster: resolvedClusterConfig,
479
704
  });
480
705
  });
481
706
  // Effect to sync features when input changes
@@ -486,22 +711,42 @@ class OlVectorLayerComponent {
486
711
  this.layerService.updateFeatures(this.id(), currentFeatures);
487
712
  }
488
713
  });
714
+ effect(() => {
715
+ this.layerService.setOpacity(this.id(), this.opacity());
716
+ });
717
+ effect(() => {
718
+ this.layerService.setVisibility(this.id(), this.visible());
719
+ });
720
+ effect(() => {
721
+ this.layerService.setZIndex(this.id(), this.zIndex());
722
+ });
723
+ // Reactive cluster distance updates
724
+ effect(() => {
725
+ const clusterCmp = this.clusterComponent();
726
+ if (clusterCmp) {
727
+ const dist = clusterCmp.distance();
728
+ const minDst = clusterCmp.minDistance();
729
+ // Since we are inside effect, these will trigger when distance changes
730
+ this.layerService.setClusterDistance(this.id(), dist);
731
+ this.layerService.setClusterMinDistance(this.id(), minDst);
732
+ }
733
+ });
489
734
  // Cleanup when component is destroyed
490
735
  this.destroyRef.onDestroy(() => {
491
736
  this.layerService.removeLayer(this.id());
492
737
  });
493
738
  }
494
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlVectorLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
495
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.4", 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 }, 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 } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
739
+ 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 });
496
741
  }
497
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlVectorLayerComponent, decorators: [{
742
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlVectorLayerComponent, decorators: [{
498
743
  type: Component,
499
744
  args: [{
500
745
  selector: 'ol-vector-layer',
501
746
  template: '',
502
747
  changeDetection: ChangeDetectionStrategy.OnPush,
503
748
  }]
504
- }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], features: [{ type: i0.Input, args: [{ isSignal: true, alias: "features", 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 }] }] } });
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 }] }] } });
505
750
 
506
751
  // OlTileLayerComponent
507
752
  class OlTileLayerComponent {
@@ -532,15 +777,24 @@ class OlTileLayerComponent {
532
777
  visible: this.visible(),
533
778
  });
534
779
  });
780
+ effect(() => {
781
+ this.layerService.setOpacity(this.id(), this.opacity());
782
+ });
783
+ effect(() => {
784
+ this.layerService.setVisibility(this.id(), this.visible());
785
+ });
786
+ effect(() => {
787
+ this.layerService.setZIndex(this.id(), this.zIndex());
788
+ });
535
789
  // Cleanup when component is destroyed
536
790
  this.destroyRef.onDestroy(() => {
537
791
  this.layerService.removeLayer(this.id());
538
792
  });
539
793
  }
540
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlTileLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
541
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.4", type: OlTileLayerComponent, isStandalone: true, selector: "ol-tile-layer", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: true, transformFunction: null }, url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: false, transformFunction: null }, attributions: { classPropertyName: "attributions", publicName: "attributions", isSignal: true, isRequired: false, transformFunction: null }, params: { classPropertyName: "params", publicName: "params", 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 } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
794
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTileLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
795
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: OlTileLayerComponent, isStandalone: true, selector: "ol-tile-layer", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: true, transformFunction: null }, url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: false, transformFunction: null }, attributions: { classPropertyName: "attributions", publicName: "attributions", isSignal: true, isRequired: false, transformFunction: null }, params: { classPropertyName: "params", publicName: "params", 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 } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
542
796
  }
543
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlTileLayerComponent, decorators: [{
797
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTileLayerComponent, decorators: [{
544
798
  type: Component,
545
799
  args: [{
546
800
  selector: 'ol-tile-layer',
@@ -583,10 +837,10 @@ class OlImageLayerComponent {
583
837
  this.layerService.removeLayer(this.id());
584
838
  });
585
839
  }
586
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlImageLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
587
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.4", type: OlImageLayerComponent, isStandalone: true, selector: "ol-image-layer", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, sourceType: { classPropertyName: "sourceType", publicName: "sourceType", isSignal: true, isRequired: true, transformFunction: null }, url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: true, transformFunction: null }, params: { classPropertyName: "params", publicName: "params", isSignal: true, isRequired: false, transformFunction: null }, imageExtent: { classPropertyName: "imageExtent", publicName: "imageExtent", 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 } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
840
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlImageLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
841
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: OlImageLayerComponent, isStandalone: true, selector: "ol-image-layer", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, sourceType: { classPropertyName: "sourceType", publicName: "sourceType", isSignal: true, isRequired: true, transformFunction: null }, url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: true, transformFunction: null }, params: { classPropertyName: "params", publicName: "params", isSignal: true, isRequired: false, transformFunction: null }, imageExtent: { classPropertyName: "imageExtent", publicName: "imageExtent", 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 } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
588
842
  }
589
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlImageLayerComponent, decorators: [{
843
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlImageLayerComponent, decorators: [{
590
844
  type: Component,
591
845
  args: [{
592
846
  selector: 'ol-image-layer',
@@ -595,6 +849,388 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
595
849
  }]
596
850
  }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], sourceType: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceType", required: true }] }], url: [{ type: i0.Input, args: [{ isSignal: true, alias: "url", required: true }] }], params: [{ type: i0.Input, args: [{ isSignal: true, alias: "params", required: false }] }], imageExtent: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageExtent", 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 }] }] } });
597
851
 
852
+ class OlHeatmapLayerComponent {
853
+ layerService = inject(OlLayerService);
854
+ mapService = inject(OlMapService);
855
+ destroyRef = inject(DestroyRef);
856
+ id = input.required(...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
857
+ features = input([], ...(ngDevMode ? [{ debugName: "features" }] : /* istanbul ignore next */ []));
858
+ zIndex = input(0, ...(ngDevMode ? [{ debugName: "zIndex" }] : /* istanbul ignore next */ []));
859
+ opacity = input(1, ...(ngDevMode ? [{ debugName: "opacity" }] : /* istanbul ignore next */ []));
860
+ visible = input(true, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
861
+ blur = input(15, ...(ngDevMode ? [{ debugName: "blur" }] : /* istanbul ignore next */ []));
862
+ radius = input(8, ...(ngDevMode ? [{ debugName: "radius" }] : /* istanbul ignore next */ []));
863
+ /** Unit for radius and blur: 'pixels' (default) or 'meters' */
864
+ radiusUnit = input('pixels', ...(ngDevMode ? [{ debugName: "radiusUnit" }] : /* istanbul ignore next */ []));
865
+ weight = input(...(ngDevMode ? [undefined, { debugName: "weight" }] : /* istanbul ignore next */ []));
866
+ /** Computed radius in pixels based on current resolution if unit is 'meters' */
867
+ scaledRadius = computed(() => {
868
+ const r = this.radius();
869
+ if (this.radiusUnit() === 'pixels')
870
+ return r;
871
+ return r / this.mapService.resolution();
872
+ }, ...(ngDevMode ? [{ debugName: "scaledRadius" }] : /* istanbul ignore next */ []));
873
+ /** Computed blur in pixels based on current resolution if unit is 'meters' */
874
+ scaledBlur = computed(() => {
875
+ const b = this.blur();
876
+ if (this.radiusUnit() === 'pixels')
877
+ return b;
878
+ return b / this.mapService.resolution();
879
+ }, ...(ngDevMode ? [{ debugName: "scaledBlur" }] : /* istanbul ignore next */ []));
880
+ constructor() {
881
+ afterNextRender(() => {
882
+ this.layerService.addLayer({
883
+ id: this.id(),
884
+ type: 'heatmap',
885
+ features: this.features(),
886
+ zIndex: this.zIndex(),
887
+ opacity: this.opacity(),
888
+ visible: this.visible(),
889
+ blur: this.scaledBlur(),
890
+ radius: this.scaledRadius(),
891
+ weight: this.weight(),
892
+ });
893
+ });
894
+ effect(() => {
895
+ const currentFeatures = this.features();
896
+ if (this.layerService.getLayer(this.id())) {
897
+ this.layerService.updateFeatures(this.id(), currentFeatures);
898
+ }
899
+ });
900
+ effect(() => {
901
+ this.layerService.setOpacity(this.id(), this.opacity());
902
+ });
903
+ effect(() => {
904
+ this.layerService.setVisibility(this.id(), this.visible());
905
+ });
906
+ effect(() => {
907
+ this.layerService.setZIndex(this.id(), this.zIndex());
908
+ });
909
+ effect(() => {
910
+ this.layerService.setHeatmapProperties(this.id(), {
911
+ blur: this.scaledBlur(),
912
+ radius: this.scaledRadius(),
913
+ weight: this.weight(),
914
+ });
915
+ });
916
+ this.destroyRef.onDestroy(() => {
917
+ this.layerService.removeLayer(this.id());
918
+ });
919
+ }
920
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlHeatmapLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
921
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: OlHeatmapLayerComponent, isStandalone: true, selector: "ol-heatmap-layer", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, features: { classPropertyName: "features", publicName: "features", 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 }, blur: { classPropertyName: "blur", publicName: "blur", isSignal: true, isRequired: false, transformFunction: null }, radius: { classPropertyName: "radius", publicName: "radius", isSignal: true, isRequired: false, transformFunction: null }, radiusUnit: { classPropertyName: "radiusUnit", publicName: "radiusUnit", isSignal: true, isRequired: false, transformFunction: null }, weight: { classPropertyName: "weight", publicName: "weight", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
922
+ }
923
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlHeatmapLayerComponent, decorators: [{
924
+ type: Component,
925
+ args: [{
926
+ selector: 'ol-heatmap-layer',
927
+ template: '',
928
+ changeDetection: ChangeDetectionStrategy.OnPush,
929
+ }]
930
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], features: [{ type: i0.Input, args: [{ isSignal: true, alias: "features", 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 }] }], blur: [{ type: i0.Input, args: [{ isSignal: true, alias: "blur", required: false }] }], radius: [{ type: i0.Input, args: [{ isSignal: true, alias: "radius", required: false }] }], radiusUnit: [{ type: i0.Input, args: [{ isSignal: true, alias: "radiusUnit", required: false }] }], weight: [{ type: i0.Input, args: [{ isSignal: true, alias: "weight", required: false }] }] } });
931
+
932
+ // OlWebGLVectorLayerComponent
933
+ // GPU-accelerated vector layer for large datasets (10k+ features).
934
+ // Uses FlatStyleLike instead of ol/style/Style. Must be manually disposed.
935
+ /**
936
+ * GPU-accelerated vector layer for rendering large datasets.
937
+ *
938
+ * Important: Uses WebGL 2 for rendering. Styles must be provided as
939
+ * `FlatStyleLike` objects (not `ol/style/Style` instances).
940
+ * Hit detection is disabled by default for performance.
941
+ *
942
+ * @usageNotes
943
+ * ```html
944
+ * <ol-webgl-vector-layer
945
+ * id="big-dataset"
946
+ * [features]="largeDataset()"
947
+ * [flatStyle]="{
948
+ * 'circle-radius': 6,
949
+ * 'circle-fill-color': ['match', ['get', 'type'], 'alert', 'red', 'blue'],
950
+ * 'stroke-color': '#333',
951
+ * 'stroke-width': 1
952
+ * }"
953
+ * [disableHitDetection]="true">
954
+ * </ol-webgl-vector-layer>
955
+ * ```
956
+ */
957
+ class OlWebGLVectorLayerComponent {
958
+ mapService = inject(OlMapService);
959
+ destroyRef = inject(DestroyRef);
960
+ /** Unique layer identifier */
961
+ id = input.required(...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
962
+ /** Features to render */
963
+ features = input([], ...(ngDevMode ? [{ debugName: "features" }] : /* istanbul ignore next */ []));
964
+ /** WebGL flat style (required — no default provided) */
965
+ flatStyle = input.required(...(ngDevMode ? [{ debugName: "flatStyle" }] : /* istanbul ignore next */ []));
966
+ /** Z-index for layer ordering */
967
+ zIndex = input(0, ...(ngDevMode ? [{ debugName: "zIndex" }] : /* istanbul ignore next */ []));
968
+ /** Opacity (0–1) */
969
+ opacity = input(1, ...(ngDevMode ? [{ debugName: "opacity" }] : /* istanbul ignore next */ []));
970
+ /** Layer visibility */
971
+ visible = input(true, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
972
+ /** Disable hit detection for better performance (default: true) */
973
+ disableHitDetection = input(true, ...(ngDevMode ? [{ debugName: "disableHitDetection" }] : /* istanbul ignore next */ []));
974
+ /** Style variables for dynamic expressions (e.g. `['var', 'threshold']`) */
975
+ variables = input(...(ngDevMode ? [undefined, { debugName: "variables" }] : /* istanbul ignore next */ []));
976
+ layer = null;
977
+ vectorSource = new VectorSource();
978
+ constructor() {
979
+ afterNextRender(() => {
980
+ const map = this.mapService.getMap();
981
+ if (!map)
982
+ return;
983
+ this.syncFeatures(this.features());
984
+ try {
985
+ this.layer = new WebGLVectorLayer({
986
+ source: this.vectorSource,
987
+ style: this.flatStyle(),
988
+ visible: this.visible(),
989
+ opacity: this.opacity(),
990
+ zIndex: this.zIndex(),
991
+ disableHitDetection: this.disableHitDetection(),
992
+ ...(this.variables() ? { variables: this.variables() } : {}),
993
+ });
994
+ this.layer.set('id', this.id());
995
+ map.addLayer(this.layer);
996
+ }
997
+ catch (err) {
998
+ // WebGL Vector Layer failed to initialize (e.g., not supported by browser)
999
+ }
1000
+ });
1001
+ // Reactive feature sync
1002
+ effect(() => {
1003
+ const currentFeatures = this.features();
1004
+ if (this.layer) {
1005
+ this.syncFeatures(currentFeatures);
1006
+ }
1007
+ });
1008
+ // Reactive style updates
1009
+ effect(() => {
1010
+ this.layer?.setStyle(this.flatStyle());
1011
+ });
1012
+ effect(() => {
1013
+ this.layer?.setOpacity(this.opacity());
1014
+ });
1015
+ effect(() => {
1016
+ this.layer?.setVisible(this.visible());
1017
+ });
1018
+ effect(() => {
1019
+ this.layer?.setZIndex(this.zIndex());
1020
+ });
1021
+ effect(() => {
1022
+ const vars = this.variables();
1023
+ if (vars && this.layer) {
1024
+ this.layer.updateStyleVariables(vars);
1025
+ }
1026
+ });
1027
+ // CRITICAL: WebGL layers must be manually disposed
1028
+ this.destroyRef.onDestroy(() => {
1029
+ const map = this.mapService.getMap();
1030
+ if (map && this.layer) {
1031
+ map.removeLayer(this.layer);
1032
+ try {
1033
+ this.layer.dispose();
1034
+ }
1035
+ catch {
1036
+ // Ignore WebGL layer disposal errors (e.g., reading 'deleteBuffer' if renderer not fully initialized)
1037
+ }
1038
+ }
1039
+ });
1040
+ }
1041
+ /**
1042
+ * Imperatively update style variables without triggering Angular change detection.
1043
+ * Ideal for 60FPS animations (e.g., linked to OlTimeService) where you don't want
1044
+ * to use the declarative [variables] input.
1045
+ */
1046
+ updateVariables(vars) {
1047
+ if (this.layer) {
1048
+ this.layer.updateStyleVariables(vars);
1049
+ }
1050
+ }
1051
+ syncFeatures(features) {
1052
+ this.vectorSource.clear();
1053
+ if (!features?.length)
1054
+ return;
1055
+ const olFeatures = features.map((feature) => {
1056
+ const geom = feature.geometry;
1057
+ let geometry;
1058
+ if (!geom.coordinates) {
1059
+ geometry = new Point([0, 0]);
1060
+ }
1061
+ else if (geom.type === 'Point') {
1062
+ geometry = new Point(fromLonLat(geom.coordinates));
1063
+ }
1064
+ else if (geom.type === 'LineString') {
1065
+ geometry = new LineString(geom.coordinates.map((c) => fromLonLat(c)));
1066
+ }
1067
+ else if (geom.type === 'Polygon') {
1068
+ geometry = new Polygon(geom.coordinates.map((ring) => ring.map((c) => fromLonLat(c))));
1069
+ }
1070
+ else if (geom.type === 'Circle') {
1071
+ const center = fromLonLat(geom.coordinates);
1072
+ geometry = new Circle$1(center, geom.radius ?? 1000);
1073
+ }
1074
+ else {
1075
+ geometry = new Point([0, 0]);
1076
+ }
1077
+ const olFeature = new OLFeature({
1078
+ geometry,
1079
+ ...feature.properties,
1080
+ });
1081
+ olFeature.setId(feature.id);
1082
+ return olFeature;
1083
+ });
1084
+ this.vectorSource.addFeatures(olFeatures);
1085
+ }
1086
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlWebGLVectorLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1087
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: OlWebGLVectorLayerComponent, isStandalone: true, selector: "ol-webgl-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 }, flatStyle: { classPropertyName: "flatStyle", publicName: "flatStyle", isSignal: true, isRequired: true, 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 }, disableHitDetection: { classPropertyName: "disableHitDetection", publicName: "disableHitDetection", isSignal: true, isRequired: false, transformFunction: null }, variables: { classPropertyName: "variables", publicName: "variables", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1088
+ }
1089
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlWebGLVectorLayerComponent, decorators: [{
1090
+ type: Component,
1091
+ args: [{
1092
+ selector: 'ol-webgl-vector-layer',
1093
+ template: '',
1094
+ changeDetection: ChangeDetectionStrategy.OnPush,
1095
+ }]
1096
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], features: [{ type: i0.Input, args: [{ isSignal: true, alias: "features", required: false }] }], flatStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "flatStyle", required: true }] }], 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 }] }], disableHitDetection: [{ type: i0.Input, args: [{ isSignal: true, alias: "disableHitDetection", required: false }] }], variables: [{ type: i0.Input, args: [{ isSignal: true, alias: "variables", required: false }] }] } });
1097
+
1098
+ // OlWebGLTileLayerComponent
1099
+ // GPU-accelerated tile layer with style expressions for color manipulation.
1100
+ /**
1101
+ * GPU-accelerated tile layer with color/brightness/contrast expressions.
1102
+ *
1103
+ * Supports the same tile sources as `ol-tile-layer` but renders via WebGL,
1104
+ * enabling GPU-powered color manipulation (brightness, contrast, saturation, gamma).
1105
+ *
1106
+ * @usageNotes
1107
+ * ```html
1108
+ * <ol-webgl-tile-layer
1109
+ * id="satellite-webgl"
1110
+ * source="xyz"
1111
+ * [url]="'https://server.arcgisonline.com/.../{z}/{y}/{x}'"
1112
+ * [tileStyle]="{ brightness: 0.1, contrast: 0.2 }">
1113
+ * </ol-webgl-tile-layer>
1114
+ * ```
1115
+ */
1116
+ class OlWebGLTileLayerComponent {
1117
+ mapService = inject(OlMapService);
1118
+ destroyRef = inject(DestroyRef);
1119
+ /** Unique layer identifier */
1120
+ id = input.required(...(ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []));
1121
+ /** Tile source type */
1122
+ source = input.required(...(ngDevMode ? [{ debugName: "source" }] : /* istanbul ignore next */ []));
1123
+ /** Tile URL template (required for 'xyz' and 'mvt') */
1124
+ url = input(...(ngDevMode ? [undefined, { debugName: "url" }] : /* istanbul ignore next */ []));
1125
+ /** Attribution text */
1126
+ attributions = input(...(ngDevMode ? [undefined, { debugName: "attributions" }] : /* istanbul ignore next */ []));
1127
+ /** WebGL tile style (raster expressions) or flat style (MVT) */
1128
+ tileStyle = input(...(ngDevMode ? [undefined, { debugName: "tileStyle" }] : /* istanbul ignore next */ []));
1129
+ /** Z-index for layer ordering */
1130
+ zIndex = input(0, ...(ngDevMode ? [{ debugName: "zIndex" }] : /* istanbul ignore next */ []));
1131
+ /** Opacity (0–1) */
1132
+ opacity = input(1, ...(ngDevMode ? [{ debugName: "opacity" }] : /* istanbul ignore next */ []));
1133
+ /** Layer visibility */
1134
+ visible = input(true, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
1135
+ /** Preload low-res tiles up to this many zoom levels */
1136
+ preload = input(0, ...(ngDevMode ? [{ debugName: "preload" }] : /* istanbul ignore next */ []));
1137
+ layer = null;
1138
+ constructor() {
1139
+ afterNextRender(() => {
1140
+ const map = this.mapService.getMap();
1141
+ if (!map)
1142
+ return;
1143
+ let tileSource;
1144
+ switch (this.source()) {
1145
+ case 'mvt':
1146
+ tileSource = new VectorTileSource({
1147
+ format: new MVT(),
1148
+ url: this.url(),
1149
+ attributions: this.attributions(),
1150
+ });
1151
+ this.layer = new WebGLVectorTileLayer({
1152
+ source: tileSource,
1153
+ visible: this.visible(),
1154
+ opacity: this.opacity(),
1155
+ zIndex: this.zIndex(),
1156
+ style: this.tileStyle() || {},
1157
+ });
1158
+ break;
1159
+ case 'xyz':
1160
+ tileSource = new XYZ({
1161
+ url: this.url(),
1162
+ attributions: this.attributions(),
1163
+ });
1164
+ this.layer = new WebGLTileLayer({
1165
+ source: tileSource,
1166
+ visible: this.visible(),
1167
+ opacity: this.opacity(),
1168
+ zIndex: this.zIndex(),
1169
+ preload: this.preload(),
1170
+ ...(this.tileStyle() ? { style: this.tileStyle() } : {}),
1171
+ });
1172
+ break;
1173
+ case 'osm':
1174
+ default:
1175
+ tileSource = new OSM({
1176
+ attributions: this.attributions(),
1177
+ });
1178
+ this.layer = new WebGLTileLayer({
1179
+ source: tileSource,
1180
+ visible: this.visible(),
1181
+ opacity: this.opacity(),
1182
+ zIndex: this.zIndex(),
1183
+ preload: this.preload(),
1184
+ ...(this.tileStyle() ? { style: this.tileStyle() } : {}),
1185
+ });
1186
+ break;
1187
+ }
1188
+ if (this.layer) {
1189
+ this.layer.set('id', this.id());
1190
+ map.addLayer(this.layer);
1191
+ }
1192
+ });
1193
+ effect(() => {
1194
+ this.layer?.setOpacity(this.opacity());
1195
+ });
1196
+ effect(() => {
1197
+ this.layer?.setVisible(this.visible());
1198
+ });
1199
+ effect(() => {
1200
+ this.layer?.setZIndex(this.zIndex());
1201
+ });
1202
+ effect(() => {
1203
+ const style = this.tileStyle();
1204
+ if (style && this.layer) {
1205
+ if (this.source() === 'mvt') {
1206
+ this.layer.setStyle(style);
1207
+ }
1208
+ else {
1209
+ this.layer.setStyle(style);
1210
+ }
1211
+ }
1212
+ });
1213
+ // WebGL tile layers also need manual dispose
1214
+ this.destroyRef.onDestroy(() => {
1215
+ const map = this.mapService.getMap();
1216
+ if (map && this.layer) {
1217
+ map.removeLayer(this.layer);
1218
+ this.layer.dispose();
1219
+ }
1220
+ });
1221
+ }
1222
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlWebGLTileLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1223
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.13", type: OlWebGLTileLayerComponent, isStandalone: true, selector: "ol-webgl-tile-layer", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: true, transformFunction: null }, url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: false, transformFunction: null }, attributions: { classPropertyName: "attributions", publicName: "attributions", isSignal: true, isRequired: false, transformFunction: null }, tileStyle: { classPropertyName: "tileStyle", publicName: "tileStyle", 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 }, preload: { classPropertyName: "preload", publicName: "preload", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
1224
+ }
1225
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlWebGLTileLayerComponent, decorators: [{
1226
+ type: Component,
1227
+ args: [{
1228
+ selector: 'ol-webgl-tile-layer',
1229
+ template: '',
1230
+ changeDetection: ChangeDetectionStrategy.OnPush,
1231
+ }]
1232
+ }], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: true }] }], url: [{ type: i0.Input, args: [{ isSignal: true, alias: "url", required: false }] }], attributions: [{ type: i0.Input, args: [{ isSignal: true, alias: "attributions", required: false }] }], tileStyle: [{ type: i0.Input, args: [{ isSignal: true, alias: "tileStyle", 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 }] }], preload: [{ type: i0.Input, args: [{ isSignal: true, alias: "preload", required: false }] }] } });
1233
+
598
1234
  // Provider functions
599
1235
  function withLayers() {
600
1236
  return { kind: 'layers', providers: [OlLayerService] };
@@ -609,4 +1245,4 @@ function provideLayers() {
609
1245
  * Generated bundle index. Do not edit.
610
1246
  */
611
1247
 
612
- export { OlImageLayerComponent, OlLayerService, OlTileLayerComponent, OlVectorLayerComponent, provideLayers, withLayers };
1248
+ export { OlClusterComponent, OlHeatmapLayerComponent, OlImageLayerComponent, OlLayerService, OlTileLayerComponent, OlVectorLayerComponent, OlWebGLTileLayerComponent, OlWebGLVectorLayerComponent, provideLayers, withLayers };