@angular-helpers/openlayers 0.5.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.
- package/README.md +119 -34
- package/core/README.md +43 -0
- package/fesm2022/angular-helpers-openlayers-core.mjs +215 -2
- package/fesm2022/angular-helpers-openlayers-interactions.mjs +121 -54
- package/fesm2022/angular-helpers-openlayers-layers.mjs +450 -365
- package/fesm2022/angular-helpers-openlayers-overlays.mjs +26 -2
- package/package.json +1 -1
- package/types/angular-helpers-openlayers-core.d.ts +73 -4
- package/types/angular-helpers-openlayers-interactions.d.ts +10 -13
- package/types/angular-helpers-openlayers-layers.d.ts +16 -16
|
@@ -1,44 +1,357 @@
|
|
|
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
|
|
21
|
-
import
|
|
22
|
-
import
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
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
|
+
}
|
|
347
|
+
|
|
36
348
|
const STYLE_PROP = '__angular_helpers_style__';
|
|
37
349
|
class OlLayerService {
|
|
38
350
|
mapService = inject(OlMapService);
|
|
39
351
|
layerCache = new Map();
|
|
40
352
|
pendingConfigs = [];
|
|
41
353
|
layerState = signal([], ...(ngDevMode ? [{ debugName: "layerState" }] : /* istanbul ignore next */ []));
|
|
354
|
+
spiderManager = new SpiderficationManager(this.layerCache);
|
|
42
355
|
layers = computed(() => this.layerState(), ...(ngDevMode ? [{ debugName: "layers" }] : /* istanbul ignore next */ []));
|
|
43
356
|
visibleLayers = computed(() => this.layerState().filter((l) => l.visible), ...(ngDevMode ? [{ debugName: "visibleLayers" }] : /* istanbul ignore next */ []));
|
|
44
357
|
tileLayers = computed(() => this.layerState().filter((l) => l.type === 'tile'), ...(ngDevMode ? [{ debugName: "tileLayers" }] : /* istanbul ignore next */ []));
|
|
@@ -64,18 +377,74 @@ class OlLayerService {
|
|
|
64
377
|
}
|
|
65
378
|
}
|
|
66
379
|
createLayer(config, map) {
|
|
380
|
+
let layer;
|
|
67
381
|
switch (config.type) {
|
|
68
|
-
case 'vector':
|
|
69
|
-
|
|
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
|
+
}
|
|
70
426
|
case 'heatmap':
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
}
|
|
76
441
|
default:
|
|
77
442
|
return { id: config.id };
|
|
78
443
|
}
|
|
444
|
+
map.addLayer(layer);
|
|
445
|
+
this.layerCache.set(config.id, layer);
|
|
446
|
+
this.updateLayerState();
|
|
447
|
+
return { id: config.id };
|
|
79
448
|
}
|
|
80
449
|
getLayer(id) {
|
|
81
450
|
return this.layerCache.get(id);
|
|
@@ -84,7 +453,6 @@ class OlLayerService {
|
|
|
84
453
|
return this.layerCache.has(id);
|
|
85
454
|
}
|
|
86
455
|
removeLayer(id) {
|
|
87
|
-
// Cancel if it's still pending (map not ready yet)
|
|
88
456
|
const pendingIdx = this.pendingConfigs.findIndex((c) => c.id === id);
|
|
89
457
|
if (pendingIdx !== -1) {
|
|
90
458
|
this.pendingConfigs.splice(pendingIdx, 1);
|
|
@@ -94,6 +462,7 @@ class OlLayerService {
|
|
|
94
462
|
const layer = this.layerCache.get(id);
|
|
95
463
|
if (map && layer) {
|
|
96
464
|
map.removeLayer(layer);
|
|
465
|
+
layer.dispose();
|
|
97
466
|
this.layerCache.delete(id);
|
|
98
467
|
this.updateLayerState();
|
|
99
468
|
}
|
|
@@ -106,9 +475,8 @@ class OlLayerService {
|
|
|
106
475
|
}
|
|
107
476
|
else {
|
|
108
477
|
const pending = this.pendingConfigs.find((c) => c.id === id);
|
|
109
|
-
if (pending)
|
|
478
|
+
if (pending)
|
|
110
479
|
pending.visible = visible;
|
|
111
|
-
}
|
|
112
480
|
}
|
|
113
481
|
}
|
|
114
482
|
toggleVisibility(id) {
|
|
@@ -129,9 +497,8 @@ class OlLayerService {
|
|
|
129
497
|
}
|
|
130
498
|
else {
|
|
131
499
|
const pending = this.pendingConfigs.find((c) => c.id === id);
|
|
132
|
-
if (pending)
|
|
500
|
+
if (pending)
|
|
133
501
|
pending.opacity = opacity;
|
|
134
|
-
}
|
|
135
502
|
}
|
|
136
503
|
}
|
|
137
504
|
setZIndex(id, zIndex) {
|
|
@@ -142,8 +509,25 @@ class OlLayerService {
|
|
|
142
509
|
}
|
|
143
510
|
else {
|
|
144
511
|
const pending = this.pendingConfigs.find((c) => c.id === id);
|
|
145
|
-
if (pending)
|
|
512
|
+
if (pending)
|
|
146
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);
|
|
147
531
|
}
|
|
148
532
|
}
|
|
149
533
|
}
|
|
@@ -179,11 +563,6 @@ class OlLayerService {
|
|
|
179
563
|
getZIndex(id) {
|
|
180
564
|
return this.layerCache.get(id)?.getZIndex() ?? 0;
|
|
181
565
|
}
|
|
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
566
|
clearFeatures(id) {
|
|
188
567
|
const layer = this.layerCache.get(id);
|
|
189
568
|
if (!(layer instanceof VectorLayer))
|
|
@@ -191,19 +570,12 @@ class OlLayerService {
|
|
|
191
570
|
const source = layer.getSource();
|
|
192
571
|
if (!source)
|
|
193
572
|
return;
|
|
194
|
-
// Handle Cluster source: clear the underlying VectorSource
|
|
195
573
|
const clusterSource = source;
|
|
196
574
|
const vectorSource = clusterSource.getSource
|
|
197
575
|
? clusterSource.getSource()
|
|
198
576
|
: source;
|
|
199
577
|
vectorSource?.clear();
|
|
200
578
|
}
|
|
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
579
|
updateFeatures(id, features) {
|
|
208
580
|
const layer = this.layerCache.get(id);
|
|
209
581
|
if (!(layer instanceof VectorLayer))
|
|
@@ -211,65 +583,32 @@ class OlLayerService {
|
|
|
211
583
|
const source = layer.getSource();
|
|
212
584
|
if (!source)
|
|
213
585
|
return;
|
|
214
|
-
// Handle Cluster source: get the underlying VectorSource
|
|
215
586
|
const clusterSource = source;
|
|
216
587
|
const vectorSource = clusterSource.getSource
|
|
217
588
|
? clusterSource.getSource()
|
|
218
589
|
: source;
|
|
219
590
|
if (!(vectorSource instanceof VectorSource))
|
|
220
591
|
return;
|
|
221
|
-
// Sync features: remove old ones, update existing ones, add new ones
|
|
222
592
|
if (features) {
|
|
223
593
|
const newFeatureIds = new Set(features.map((f) => f.id));
|
|
224
594
|
const sourceFeatures = vectorSource.getFeatures();
|
|
225
|
-
// 1. Remove features that are no longer in the input
|
|
226
595
|
sourceFeatures.forEach((f) => {
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
596
|
+
const fId = f.getId();
|
|
597
|
+
if (fId !== undefined && !newFeatureIds.has(fId)) {
|
|
229
598
|
vectorSource.removeFeature(f);
|
|
230
599
|
}
|
|
231
600
|
});
|
|
232
|
-
// 2. Only add features that don't already exist in the source
|
|
233
601
|
const existingIds = new Set(vectorSource
|
|
234
602
|
.getFeatures()
|
|
235
603
|
.map((f) => f.getId())
|
|
236
|
-
.filter((
|
|
604
|
+
.filter((fId) => fId !== undefined));
|
|
237
605
|
const featuresToAdd = features.filter((f) => !existingIds.has(f.id));
|
|
238
606
|
if (featuresToAdd.length > 0) {
|
|
239
|
-
const olFeatures = featuresToAdd.map((
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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,
|
|
267
|
-
});
|
|
268
|
-
if (feature.style) {
|
|
269
|
-
olFeature.set(STYLE_PROP, feature.style);
|
|
270
|
-
}
|
|
271
|
-
olFeature.setId(feature.id);
|
|
272
|
-
return olFeature;
|
|
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;
|
|
273
612
|
});
|
|
274
613
|
vectorSource.addFeatures(olFeatures);
|
|
275
614
|
}
|
|
@@ -295,285 +634,6 @@ class OlLayerService {
|
|
|
295
634
|
});
|
|
296
635
|
this.layerState.set(layers.sort((a, b) => a.zIndex - b.zIndex));
|
|
297
636
|
}
|
|
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
637
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlLayerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
578
638
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlLayerService });
|
|
579
639
|
}
|
|
@@ -586,8 +646,10 @@ class OlClusterComponent {
|
|
|
586
646
|
minDistance = input(20, ...(ngDevMode ? [{ debugName: "minDistance" }] : /* istanbul ignore next */ []));
|
|
587
647
|
showCount = input(true, ...(ngDevMode ? [{ debugName: "showCount" }] : /* istanbul ignore next */ []));
|
|
588
648
|
featureStyle = input(...(ngDevMode ? [undefined, { debugName: "featureStyle" }] : /* istanbul ignore next */ []));
|
|
649
|
+
spiderfyOnSelect = input(false, ...(ngDevMode ? [{ debugName: "spiderfyOnSelect" }] : /* istanbul ignore next */ []));
|
|
650
|
+
spiderfyClick = output();
|
|
589
651
|
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 });
|
|
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 });
|
|
591
653
|
}
|
|
592
654
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlClusterComponent, decorators: [{
|
|
593
655
|
type: Component,
|
|
@@ -596,7 +658,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
|
|
|
596
658
|
template: '',
|
|
597
659
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
598
660
|
}]
|
|
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 }] }] } });
|
|
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"] }] } });
|
|
600
662
|
|
|
601
663
|
// OlVectorLayerComponent
|
|
602
664
|
class OlVectorLayerComponent {
|
|
@@ -624,6 +686,8 @@ class OlVectorLayerComponent {
|
|
|
624
686
|
minDistance: clusterCmp.minDistance(),
|
|
625
687
|
showCount: clusterCmp.showCount(),
|
|
626
688
|
featureStyle: clusterCmp.featureStyle(),
|
|
689
|
+
spiderfyOnSelect: clusterCmp.spiderfyOnSelect(),
|
|
690
|
+
onSpiderfyClick: (f) => clusterCmp.spiderfyClick.emit(f),
|
|
627
691
|
}
|
|
628
692
|
: undefined);
|
|
629
693
|
this.layerService.addLayer({
|
|
@@ -656,6 +720,17 @@ class OlVectorLayerComponent {
|
|
|
656
720
|
effect(() => {
|
|
657
721
|
this.layerService.setZIndex(this.id(), this.zIndex());
|
|
658
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
|
+
});
|
|
659
734
|
// Cleanup when component is destroyed
|
|
660
735
|
this.destroyRef.onDestroy(() => {
|
|
661
736
|
this.layerService.removeLayer(this.id());
|
|
@@ -963,6 +1038,16 @@ class OlWebGLVectorLayerComponent {
|
|
|
963
1038
|
}
|
|
964
1039
|
});
|
|
965
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
|
+
}
|
|
966
1051
|
syncFeatures(features) {
|
|
967
1052
|
this.vectorSource.clear();
|
|
968
1053
|
if (!features?.length)
|
|
@@ -984,7 +1069,7 @@ class OlWebGLVectorLayerComponent {
|
|
|
984
1069
|
}
|
|
985
1070
|
else if (geom.type === 'Circle') {
|
|
986
1071
|
const center = fromLonLat(geom.coordinates);
|
|
987
|
-
geometry = new Circle(center, geom.radius ?? 1000);
|
|
1072
|
+
geometry = new Circle$1(center, geom.radius ?? 1000);
|
|
988
1073
|
}
|
|
989
1074
|
else {
|
|
990
1075
|
geometry = new Point([0, 0]);
|