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