@angular-helpers/openlayers 0.2.0 → 0.4.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 -2
- package/fesm2022/angular-helpers-openlayers-core.mjs +18 -0
- package/fesm2022/angular-helpers-openlayers-interactions.mjs +12 -0
- package/fesm2022/angular-helpers-openlayers-layers.mjs +141 -13
- package/fesm2022/angular-helpers-openlayers-military.mjs +223 -6
- package/fesm2022/angular-helpers-openlayers-overlays.mjs +439 -8
- package/package.json +6 -2
- package/types/angular-helpers-openlayers-core.d.ts +17 -0
- package/types/angular-helpers-openlayers-interactions.d.ts +7 -0
- package/types/angular-helpers-openlayers-layers.d.ts +48 -34
- package/types/angular-helpers-openlayers-military.d.ts +156 -2
- package/types/angular-helpers-openlayers-overlays.d.ts +196 -11
package/README.md
CHANGED
|
@@ -97,6 +97,173 @@ export class MapComponent {
|
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
## Overlays — popups and tooltips
|
|
101
|
+
|
|
102
|
+
Available since `0.3.0` from `@angular-helpers/openlayers/overlays`.
|
|
103
|
+
|
|
104
|
+
### `<ol-popup>` — declarative popup with content projection
|
|
105
|
+
|
|
106
|
+
The popup's host element is used directly as the underlying `ol/Overlay` element, so projected children stay inside Angular's view tree and benefit from change detection without any extra plumbing.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { OlPopupComponent } from '@angular-helpers/openlayers/overlays';
|
|
110
|
+
|
|
111
|
+
@Component({
|
|
112
|
+
imports: [OlMapComponent, OlVectorLayerComponent, OlPopupComponent],
|
|
113
|
+
template: `
|
|
114
|
+
<ol-map [center]="[2.17, 41.38]" [zoom]="12">
|
|
115
|
+
<ol-vector-layer id="cities" [features]="cities()" />
|
|
116
|
+
|
|
117
|
+
<ol-popup
|
|
118
|
+
[position]="selectedCoord()"
|
|
119
|
+
[closeButton]="true"
|
|
120
|
+
[autoPan]="true"
|
|
121
|
+
(closed)="clearSelection()"
|
|
122
|
+
>
|
|
123
|
+
<h3>{{ selected()?.name }}</h3>
|
|
124
|
+
<p>{{ selected()?.description }}</p>
|
|
125
|
+
</ol-popup>
|
|
126
|
+
</ol-map>
|
|
127
|
+
`,
|
|
128
|
+
})
|
|
129
|
+
export class MyMap {
|
|
130
|
+
// …
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Setting `[position]="null"` hides the popup and emits `closed`.
|
|
135
|
+
|
|
136
|
+
### `[olTooltip]` — feature hover tooltip
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<ol-vector-layer
|
|
140
|
+
id="cities"
|
|
141
|
+
[features]="cities()"
|
|
142
|
+
[olTooltip]="'name'"
|
|
143
|
+
[olTooltipLayer]="'cities'"
|
|
144
|
+
/>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Reads `feature.get('name')` for any feature on layer `cities` under the cursor and renders a styled `<div role="tooltip">` near the pointer. Use the `.ol-tooltip` class to override the default look.
|
|
148
|
+
|
|
149
|
+
### `OlPopupService` — programmatic popups
|
|
150
|
+
|
|
151
|
+
Three content modes from a service:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const popups = inject(OlPopupService);
|
|
155
|
+
|
|
156
|
+
// 1) Plain text / HTMLElement
|
|
157
|
+
popups.open({
|
|
158
|
+
id: 'simple',
|
|
159
|
+
position: [2.17, 41.38],
|
|
160
|
+
content: 'Hello map',
|
|
161
|
+
positioning: 'bottom-center',
|
|
162
|
+
autoPan: true,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 2) Dynamic Angular component (createComponent + hostElement)
|
|
166
|
+
const handle = popups.openComponent({
|
|
167
|
+
id: 'city-popup',
|
|
168
|
+
position: [2.17, 41.38],
|
|
169
|
+
component: CityCardComponent,
|
|
170
|
+
bindings: [
|
|
171
|
+
inputBinding('city', () => selected()),
|
|
172
|
+
outputBinding<void>('closed', () => handle.close()),
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`open` is idempotent by `id` and updates the existing overlay in place. `openComponent` always recreates the `ComponentRef` on a repeated id and disposes the previous one (`appRef.detachView` + `ref.destroy`) to avoid CD leaks. Calls made before the map is ready are queued and replayed on `OlMapService.onReady`.
|
|
178
|
+
|
|
179
|
+
## Military symbology
|
|
180
|
+
|
|
181
|
+
Available since `0.4.0` from `@angular-helpers/openlayers/military`.
|
|
182
|
+
|
|
183
|
+
Three pure-math geometry helpers (no extra deps) plus a NATO MIL-STD-2525 symbol helper backed by the optional [`milsymbol`](https://github.com/spatialillusions/milsymbol) peer dependency.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { inject, signal } from '@angular/core';
|
|
187
|
+
import { OlMilitaryService } from '@angular-helpers/openlayers/military';
|
|
188
|
+
import type { Feature } from '@angular-helpers/openlayers/core';
|
|
189
|
+
|
|
190
|
+
@Component({
|
|
191
|
+
// …
|
|
192
|
+
imports: [OlMapComponent, OlVectorLayerComponent],
|
|
193
|
+
template: `
|
|
194
|
+
<ol-map [center]="[2.17, 41.38]" [zoom]="8">
|
|
195
|
+
<ol-tile-layer id="osm" source="osm" />
|
|
196
|
+
<ol-vector-layer id="military" [features]="features()" [zIndex]="10" />
|
|
197
|
+
</ol-map>
|
|
198
|
+
`,
|
|
199
|
+
})
|
|
200
|
+
export class MilDemo {
|
|
201
|
+
private ml = inject(OlMilitaryService);
|
|
202
|
+
features = signal<Feature[]>([]);
|
|
203
|
+
|
|
204
|
+
async ngOnInit() {
|
|
205
|
+
const ellipse = this.ml.createEllipse({
|
|
206
|
+
center: [2.17, 41.38],
|
|
207
|
+
semiMajor: 6_000,
|
|
208
|
+
semiMinor: 3_000,
|
|
209
|
+
rotation: Math.PI / 6,
|
|
210
|
+
});
|
|
211
|
+
const sector = this.ml.createSector({
|
|
212
|
+
center: [-0.38, 39.47],
|
|
213
|
+
radius: 8_000,
|
|
214
|
+
startAngle: Math.PI / 6,
|
|
215
|
+
endAngle: Math.PI / 2,
|
|
216
|
+
});
|
|
217
|
+
const donut = this.ml.createDonut({
|
|
218
|
+
center: [-5.99, 37.39],
|
|
219
|
+
innerRadius: 5_000,
|
|
220
|
+
outerRadius: 10_000,
|
|
221
|
+
});
|
|
222
|
+
const symbol = await this.ml.createMilSymbol({
|
|
223
|
+
sidc: 'SFGPUCI-----',
|
|
224
|
+
position: [-3.7, 40.42],
|
|
225
|
+
size: 36,
|
|
226
|
+
});
|
|
227
|
+
this.features.set([ellipse, sector, donut, symbol]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Geometry helpers
|
|
233
|
+
|
|
234
|
+
| Method | Output | Notes |
|
|
235
|
+
| ----------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
|
236
|
+
| `createEllipse(config)` | `Feature<Polygon>` | Optional `rotation` in radians, configurable `segments` (default 64) |
|
|
237
|
+
| `createSector(config)` | `Feature<Polygon>` | Pie-slice (apex-arc-apex). `startAngle < endAngle ≤ start + 2π` |
|
|
238
|
+
| `createDonut(config)` | `Feature<Polygon>` | Two rings: outer CCW, inner CW (right-hand rule). Renders as an annular band with the basemap visible through the hole |
|
|
239
|
+
|
|
240
|
+
Coordinates are emitted in `EPSG:4326` (lon/lat) using a local tangent-plane projection. Accurate up to ~100 km from the center; for very large radii or polar regions, geodesic-correct math is on the Phase 3 roadmap.
|
|
241
|
+
|
|
242
|
+
### MIL-STD-2525 symbols
|
|
243
|
+
|
|
244
|
+
`createMilSymbol` lazy-loads `milsymbol` on first use and returns a `Feature<Point>` with style metadata (`feature.style.icon`) so the vector layer renders it as an `ol/style/Icon`. The library is declared as an **optional peer dependency** — install it only if you use this helper:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
npm install milsymbol
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const symbol = await ml.createMilSymbol({
|
|
252
|
+
sidc: 'SFGPUCI-----', // friendly infantry, ground unit
|
|
253
|
+
position: [-3.7, 40.42],
|
|
254
|
+
size: 36,
|
|
255
|
+
uniqueDesignation: 'A1',
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Three flavors:
|
|
260
|
+
|
|
261
|
+
- **`createMilSymbol(config)`** — async; lazy-loads on first call.
|
|
262
|
+
- **`createMilSymbolSync(config)`** — sync; throws if `milsymbol` is not loaded yet.
|
|
263
|
+
- **`preloadMilsymbol()`** — fire-and-forget on app init to make the first symbol render synchronous.
|
|
264
|
+
|
|
265
|
+
The service throws clearly on non-browser environments (`createMilSymbol` requires `window`).
|
|
266
|
+
|
|
100
267
|
## Architecture
|
|
101
268
|
|
|
102
269
|
### Data vs UI Separation
|
|
@@ -127,8 +294,8 @@ import {
|
|
|
127
294
|
withInteractions,
|
|
128
295
|
} from '@angular-helpers/openlayers/interactions';
|
|
129
296
|
|
|
130
|
-
// Add military features
|
|
131
|
-
import {
|
|
297
|
+
// Add military features — pure-math helpers + lazy-loaded milsymbol
|
|
298
|
+
import { OlMilitaryService, withMilitary } from '@angular-helpers/openlayers/military';
|
|
132
299
|
```
|
|
133
300
|
|
|
134
301
|
## API Reference
|
|
@@ -155,6 +155,7 @@ class OlMapComponent {
|
|
|
155
155
|
mapDblClick = output();
|
|
156
156
|
mapContainerRef = viewChild.required('mapContainer');
|
|
157
157
|
map;
|
|
158
|
+
resizeObserver;
|
|
158
159
|
constructor() {
|
|
159
160
|
afterNextRender(() => this.initMap());
|
|
160
161
|
effect(() => {
|
|
@@ -186,6 +187,19 @@ class OlMapComponent {
|
|
|
186
187
|
});
|
|
187
188
|
this.map = new OLMap({ target: container, view, layers: [] });
|
|
188
189
|
this.mapService.setMap(this.map);
|
|
190
|
+
// Add ResizeObserver to handle container size changes (e.g. sidebars, window resize)
|
|
191
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
192
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
193
|
+
if (this.map) {
|
|
194
|
+
// Using requestAnimationFrame prevents "ResizeObserver loop limit exceeded" errors
|
|
195
|
+
requestAnimationFrame(() => {
|
|
196
|
+
if (this.map)
|
|
197
|
+
this.map.updateSize();
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
this.resizeObserver.observe(container);
|
|
202
|
+
}
|
|
189
203
|
view.on('change:center', () => this.zoneHelper.runInsideAngular(() => this.emitViewChange()));
|
|
190
204
|
view.on('change:resolution', () => this.zoneHelper.runInsideAngular(() => this.emitViewChange()));
|
|
191
205
|
this.map.on('click', (e) => this.zoneHelper.runInsideAngular(() => this.mapClick.emit({
|
|
@@ -200,6 +214,10 @@ class OlMapComponent {
|
|
|
200
214
|
this.emitViewChange();
|
|
201
215
|
}
|
|
202
216
|
destroyMap() {
|
|
217
|
+
if (this.resizeObserver) {
|
|
218
|
+
this.resizeObserver.disconnect();
|
|
219
|
+
this.resizeObserver = undefined;
|
|
220
|
+
}
|
|
203
221
|
if (this.map) {
|
|
204
222
|
this.zoneHelper.runOutsideAngular(() => {
|
|
205
223
|
this.map.setTarget(undefined);
|
|
@@ -33,9 +33,20 @@ class InteractionStateService {
|
|
|
33
33
|
modify$ = this.modifySubject.asObservable();
|
|
34
34
|
/**
|
|
35
35
|
* Adds a managed interaction to the state.
|
|
36
|
+
* If the interaction is marked as exclusive, it disables other exclusive interactions.
|
|
36
37
|
* @param interaction - The interaction to add
|
|
37
38
|
*/
|
|
38
39
|
addInteraction(interaction) {
|
|
40
|
+
if (interaction.config.exclusive !== false) {
|
|
41
|
+
// Disable other exclusive interactions to maintain mutual exclusivity
|
|
42
|
+
const currentInteractions = this.interactions();
|
|
43
|
+
for (const existing of currentInteractions) {
|
|
44
|
+
if (existing.id !== interaction.id && existing.config.exclusive !== false) {
|
|
45
|
+
existing.cleanup();
|
|
46
|
+
this.removeInteraction(existing.id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
39
50
|
this.interactions.update((list) => [...list, interaction]);
|
|
40
51
|
}
|
|
41
52
|
/**
|
|
@@ -533,6 +544,7 @@ function withInteractions() {
|
|
|
533
544
|
return {
|
|
534
545
|
kind: 'interactions',
|
|
535
546
|
providers: [
|
|
547
|
+
OlLayerService, // DX: interactions often need layers
|
|
536
548
|
OlInteractionService,
|
|
537
549
|
InteractionStateService,
|
|
538
550
|
SelectInteractionService,
|
|
@@ -4,18 +4,27 @@ import VectorLayer from 'ol/layer/Vector';
|
|
|
4
4
|
import TileLayer from 'ol/layer/Tile';
|
|
5
5
|
import ImageLayer from 'ol/layer/Image';
|
|
6
6
|
import VectorSource from 'ol/source/Vector';
|
|
7
|
-
import
|
|
7
|
+
import OLFeature from 'ol/Feature';
|
|
8
8
|
import { Point, LineString, Polygon, Circle } from 'ol/geom';
|
|
9
9
|
import { fromLonLat } from 'ol/proj';
|
|
10
|
-
import {
|
|
10
|
+
import { getCenter } from 'ol/extent';
|
|
11
|
+
import { Style, Circle as Circle$1, Stroke, Fill, Icon } from 'ol/style';
|
|
12
|
+
import Text from 'ol/style/Text';
|
|
13
|
+
import ClusterSource from 'ol/source/Cluster';
|
|
11
14
|
import OSM from 'ol/source/OSM';
|
|
12
15
|
import XYZ from 'ol/source/XYZ';
|
|
13
16
|
import TileWMS from 'ol/source/TileWMS';
|
|
14
17
|
import ImageWMS from 'ol/source/ImageWMS';
|
|
15
|
-
import
|
|
18
|
+
import ImageStatic from 'ol/source/ImageStatic';
|
|
16
19
|
import { OlMapService } from '@angular-helpers/openlayers/core';
|
|
17
20
|
|
|
18
21
|
// OlLayerService
|
|
22
|
+
/**
|
|
23
|
+
* Internal property key used to stash the abstract style metadata on the
|
|
24
|
+
* underlying `ol/Feature` so the layer style function can resolve a
|
|
25
|
+
* per-feature visual without colliding with user `properties`.
|
|
26
|
+
*/
|
|
27
|
+
const STYLE_PROP = '__angular_helpers_style__';
|
|
19
28
|
class OlLayerService {
|
|
20
29
|
mapService = inject(OlMapService);
|
|
21
30
|
layerCache = new Map();
|
|
@@ -127,7 +136,15 @@ class OlLayerService {
|
|
|
127
136
|
const layer = this.layerCache.get(id);
|
|
128
137
|
if (!(layer instanceof VectorLayer))
|
|
129
138
|
return;
|
|
130
|
-
layer.getSource()
|
|
139
|
+
const source = layer.getSource();
|
|
140
|
+
if (!source)
|
|
141
|
+
return;
|
|
142
|
+
// Handle Cluster source: clear the underlying VectorSource
|
|
143
|
+
const clusterSource = source;
|
|
144
|
+
const vectorSource = clusterSource.getSource
|
|
145
|
+
? clusterSource.getSource()
|
|
146
|
+
: source;
|
|
147
|
+
vectorSource?.clear();
|
|
131
148
|
}
|
|
132
149
|
/**
|
|
133
150
|
* Updates the features of a vector layer.
|
|
@@ -142,8 +159,15 @@ class OlLayerService {
|
|
|
142
159
|
const source = layer.getSource();
|
|
143
160
|
if (!source)
|
|
144
161
|
return;
|
|
162
|
+
// Handle Cluster source: get the underlying VectorSource
|
|
163
|
+
const clusterSource = source;
|
|
164
|
+
const vectorSource = clusterSource.getSource
|
|
165
|
+
? clusterSource.getSource()
|
|
166
|
+
: source;
|
|
167
|
+
if (!(vectorSource instanceof VectorSource))
|
|
168
|
+
return;
|
|
145
169
|
// Get existing feature IDs from source
|
|
146
|
-
const existingIds = new Set(
|
|
170
|
+
const existingIds = new Set(vectorSource
|
|
147
171
|
.getFeatures()
|
|
148
172
|
.map((f) => f.getId())
|
|
149
173
|
.filter((id) => id !== undefined));
|
|
@@ -178,14 +202,17 @@ class OlLayerService {
|
|
|
178
202
|
else {
|
|
179
203
|
geometry = new Point([0, 0]);
|
|
180
204
|
}
|
|
181
|
-
const olFeature = new
|
|
205
|
+
const olFeature = new OLFeature({
|
|
182
206
|
geometry,
|
|
183
207
|
...feature.properties,
|
|
184
208
|
});
|
|
209
|
+
if (feature.style) {
|
|
210
|
+
olFeature.set(STYLE_PROP, feature.style);
|
|
211
|
+
}
|
|
185
212
|
olFeature.setId(feature.id);
|
|
186
213
|
return olFeature;
|
|
187
214
|
});
|
|
188
|
-
|
|
215
|
+
vectorSource.addFeatures(olFeatures);
|
|
189
216
|
}
|
|
190
217
|
}
|
|
191
218
|
}
|
|
@@ -204,7 +231,7 @@ class OlLayerService {
|
|
|
204
231
|
this.layerState.set(layers.sort((a, b) => a.zIndex - b.zIndex));
|
|
205
232
|
}
|
|
206
233
|
createVectorLayer(config, map) {
|
|
207
|
-
const
|
|
234
|
+
const vectorSource = new VectorSource();
|
|
208
235
|
// Add features if provided
|
|
209
236
|
if (config.features && config.features.length > 0) {
|
|
210
237
|
const olFeatures = config.features.map((feature) => {
|
|
@@ -234,15 +261,40 @@ class OlLayerService {
|
|
|
234
261
|
else {
|
|
235
262
|
geometry = new Point([0, 0]);
|
|
236
263
|
}
|
|
237
|
-
const olFeature = new
|
|
264
|
+
const olFeature = new OLFeature({
|
|
238
265
|
geometry,
|
|
239
266
|
...feature.properties,
|
|
240
267
|
});
|
|
268
|
+
if (feature.style) {
|
|
269
|
+
olFeature.set(STYLE_PROP, feature.style);
|
|
270
|
+
}
|
|
241
271
|
olFeature.setId(feature.id);
|
|
242
272
|
return olFeature;
|
|
243
273
|
});
|
|
244
|
-
|
|
274
|
+
vectorSource.addFeatures(olFeatures);
|
|
245
275
|
}
|
|
276
|
+
// Wrap in cluster source if enabled
|
|
277
|
+
const clusterCfg = config.cluster;
|
|
278
|
+
const source = clusterCfg?.enabled
|
|
279
|
+
? new ClusterSource({
|
|
280
|
+
source: vectorSource,
|
|
281
|
+
distance: clusterCfg.distance ?? 40,
|
|
282
|
+
minDistance: clusterCfg.minDistance ?? 20,
|
|
283
|
+
geometryFunction: (feature) => {
|
|
284
|
+
const geometry = feature.getGeometry();
|
|
285
|
+
if (!geometry)
|
|
286
|
+
return null;
|
|
287
|
+
// For Point geometries, use as-is
|
|
288
|
+
if (geometry.getType() === 'Point') {
|
|
289
|
+
return geometry;
|
|
290
|
+
}
|
|
291
|
+
// For other geometries (Polygon, Circle, etc.), use center point
|
|
292
|
+
const extent = geometry.getExtent();
|
|
293
|
+
const center = getCenter(extent);
|
|
294
|
+
return new Point(center);
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
: vectorSource;
|
|
246
298
|
// Default style for all geometry types (points, lines, polygons)
|
|
247
299
|
const defaultStyle = new Style({
|
|
248
300
|
fill: new Fill({ color: 'rgba(25, 118, 210, 0.3)' }),
|
|
@@ -253,12 +305,86 @@ class OlLayerService {
|
|
|
253
305
|
stroke: new Stroke({ color: '#d32f2f', width: 2 }),
|
|
254
306
|
}),
|
|
255
307
|
});
|
|
308
|
+
// Cluster style: shows count badge when features are clustered
|
|
309
|
+
const clusterStyleFn = (olFeature) => {
|
|
310
|
+
const features = olFeature.get('features');
|
|
311
|
+
const size = features?.length ?? 1;
|
|
312
|
+
if (size > 1) {
|
|
313
|
+
const showCount = clusterCfg?.showCount ?? true;
|
|
314
|
+
return new Style({
|
|
315
|
+
image: new Circle$1({
|
|
316
|
+
radius: 15 + Math.min(size * 2, 15),
|
|
317
|
+
fill: new Fill({ color: 'rgba(255, 100, 100, 0.8)' }),
|
|
318
|
+
stroke: new Stroke({ color: '#fff', width: 2 }),
|
|
319
|
+
}),
|
|
320
|
+
text: showCount
|
|
321
|
+
? new Text({
|
|
322
|
+
text: String(size),
|
|
323
|
+
fill: new Fill({ color: '#fff' }),
|
|
324
|
+
})
|
|
325
|
+
: undefined,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
// Single feature: get the original feature from the cluster and use its style
|
|
329
|
+
const originalFeatures = olFeature.get('features');
|
|
330
|
+
const originalFeature = originalFeatures?.[0];
|
|
331
|
+
if (originalFeature) {
|
|
332
|
+
const abstractStyle = originalFeature.get(STYLE_PROP);
|
|
333
|
+
if (abstractStyle) {
|
|
334
|
+
const style = new Style();
|
|
335
|
+
const { icon, fill, stroke } = abstractStyle;
|
|
336
|
+
if (icon?.src) {
|
|
337
|
+
style.setImage(new Icon({
|
|
338
|
+
src: icon.src,
|
|
339
|
+
...(icon.size ? { size: icon.size } : {}),
|
|
340
|
+
...(icon.anchor ? { anchor: icon.anchor } : {}),
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
if (fill) {
|
|
344
|
+
style.setFill(new Fill({ color: fill.color }));
|
|
345
|
+
}
|
|
346
|
+
if (stroke) {
|
|
347
|
+
style.setStroke(new Stroke({ color: stroke.color, width: stroke.width }));
|
|
348
|
+
}
|
|
349
|
+
// If we mapped at least one property, return it, otherwise fallback
|
|
350
|
+
if (icon?.src || fill || stroke)
|
|
351
|
+
return style;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return defaultStyle;
|
|
355
|
+
};
|
|
356
|
+
// Per-feature style resolver: features carrying `style` render it.
|
|
357
|
+
// Structural type avoids importing `FeatureLike` from `ol/Feature`;
|
|
358
|
+
// tooling has been observed to auto-remove the unused-looking import.
|
|
359
|
+
const styleFn = (olFeature) => {
|
|
360
|
+
const abstractStyle = olFeature.get(STYLE_PROP);
|
|
361
|
+
if (abstractStyle) {
|
|
362
|
+
const style = new Style();
|
|
363
|
+
const { icon, fill, stroke } = abstractStyle;
|
|
364
|
+
if (icon?.src) {
|
|
365
|
+
style.setImage(new Icon({
|
|
366
|
+
src: icon.src,
|
|
367
|
+
...(icon.size ? { size: icon.size } : {}),
|
|
368
|
+
...(icon.anchor ? { anchor: icon.anchor } : {}),
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
if (fill) {
|
|
372
|
+
style.setFill(new Fill({ color: fill.color }));
|
|
373
|
+
}
|
|
374
|
+
if (stroke) {
|
|
375
|
+
style.setStroke(new Stroke({ color: stroke.color, width: stroke.width }));
|
|
376
|
+
}
|
|
377
|
+
if (icon?.src || fill || stroke)
|
|
378
|
+
return style;
|
|
379
|
+
}
|
|
380
|
+
return defaultStyle;
|
|
381
|
+
};
|
|
256
382
|
const layer = new VectorLayer({
|
|
257
383
|
source,
|
|
258
384
|
visible: config.visible ?? true,
|
|
259
385
|
opacity: config.opacity ?? 1,
|
|
260
386
|
zIndex: config.zIndex,
|
|
261
|
-
style:
|
|
387
|
+
style: clusterCfg?.enabled ? clusterStyleFn : styleFn,
|
|
262
388
|
});
|
|
263
389
|
layer.set('id', config.id);
|
|
264
390
|
map.addLayer(layer);
|
|
@@ -337,6 +463,7 @@ class OlVectorLayerComponent {
|
|
|
337
463
|
opacity = input(1, ...(ngDevMode ? [{ debugName: "opacity" }] : /* istanbul ignore next */ []));
|
|
338
464
|
visible = input(true, ...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
339
465
|
style = input(...(ngDevMode ? [undefined, { debugName: "style" }] : /* istanbul ignore next */ []));
|
|
466
|
+
cluster = input(...(ngDevMode ? [undefined, { debugName: "cluster" }] : /* istanbul ignore next */ []));
|
|
340
467
|
constructor() {
|
|
341
468
|
// Initialize layer after DOM is ready
|
|
342
469
|
afterNextRender(() => {
|
|
@@ -348,6 +475,7 @@ class OlVectorLayerComponent {
|
|
|
348
475
|
opacity: this.opacity(),
|
|
349
476
|
visible: this.visible(),
|
|
350
477
|
style: this.style(),
|
|
478
|
+
cluster: this.cluster(),
|
|
351
479
|
});
|
|
352
480
|
});
|
|
353
481
|
// Effect to sync features when input changes
|
|
@@ -364,7 +492,7 @@ class OlVectorLayerComponent {
|
|
|
364
492
|
});
|
|
365
493
|
}
|
|
366
494
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlVectorLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
367
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.4", type: OlVectorLayerComponent, isStandalone: true, selector: "ol-vector-layer", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, features: { classPropertyName: "features", publicName: "features", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, opacity: { classPropertyName: "opacity", publicName: "opacity", isSignal: true, isRequired: false, transformFunction: null }, visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, style: { classPropertyName: "style", publicName: "style", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
495
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.4", type: OlVectorLayerComponent, isStandalone: true, selector: "ol-vector-layer", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, features: { classPropertyName: "features", publicName: "features", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, opacity: { classPropertyName: "opacity", publicName: "opacity", isSignal: true, isRequired: false, transformFunction: null }, visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, style: { classPropertyName: "style", publicName: "style", isSignal: true, isRequired: false, transformFunction: null }, cluster: { classPropertyName: "cluster", publicName: "cluster", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
368
496
|
}
|
|
369
497
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlVectorLayerComponent, decorators: [{
|
|
370
498
|
type: Component,
|
|
@@ -373,7 +501,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImpor
|
|
|
373
501
|
template: '',
|
|
374
502
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
375
503
|
}]
|
|
376
|
-
}], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], features: [{ type: i0.Input, args: [{ isSignal: true, alias: "features", required: false }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], opacity: [{ type: i0.Input, args: [{ isSignal: true, alias: "opacity", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], style: [{ type: i0.Input, args: [{ isSignal: true, alias: "style", required: false }] }] } });
|
|
504
|
+
}], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], features: [{ type: i0.Input, args: [{ isSignal: true, alias: "features", required: false }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }], opacity: [{ type: i0.Input, args: [{ isSignal: true, alias: "opacity", required: false }] }], visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }], style: [{ type: i0.Input, args: [{ isSignal: true, alias: "style", required: false }] }], cluster: [{ type: i0.Input, args: [{ isSignal: true, alias: "cluster", required: false }] }] } });
|
|
377
505
|
|
|
378
506
|
// OlTileLayerComponent
|
|
379
507
|
class OlTileLayerComponent {
|