@angular-helpers/openlayers 0.3.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 +90 -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 +140 -12
- package/fesm2022/angular-helpers-openlayers-military.mjs +223 -6
- package/fesm2022/angular-helpers-openlayers-overlays.mjs +2 -1
- package/package.json +5 -1
- 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/README.md
CHANGED
|
@@ -176,6 +176,94 @@ const handle = popups.openComponent({
|
|
|
176
176
|
|
|
177
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
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
|
+
|
|
179
267
|
## Architecture
|
|
180
268
|
|
|
181
269
|
### Data vs UI Separation
|
|
@@ -206,8 +294,8 @@ import {
|
|
|
206
294
|
withInteractions,
|
|
207
295
|
} from '@angular-helpers/openlayers/interactions';
|
|
208
296
|
|
|
209
|
-
// Add military features
|
|
210
|
-
import {
|
|
297
|
+
// Add military features — pure-math helpers + lazy-loaded milsymbol
|
|
298
|
+
import { OlMilitaryService, withMilitary } from '@angular-helpers/openlayers/military';
|
|
211
299
|
```
|
|
212
300
|
|
|
213
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,10 +4,13 @@ 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';
|
|
@@ -16,6 +19,12 @@ 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 {
|
|
@@ -1,20 +1,237 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { Injectable } from '@angular/core';
|
|
3
3
|
|
|
4
|
-
//
|
|
4
|
+
// @angular-helpers/openlayers/military — service implementation
|
|
5
|
+
/**
|
|
6
|
+
* Meters per degree of latitude on a spherical Earth approximation.
|
|
7
|
+
* Used by the local tangent-plane projection in the geometry helpers.
|
|
8
|
+
*/
|
|
9
|
+
const METERS_PER_DEGREE_LAT = 111_320;
|
|
10
|
+
/**
|
|
11
|
+
* Service exposing geometry helpers and MIL-STD-2525 symbology rendering.
|
|
12
|
+
*
|
|
13
|
+
* - `createEllipse`, `createSector`, `createDonut` are **pure math** and
|
|
14
|
+
* have no runtime dependencies beyond the bundled types.
|
|
15
|
+
* - `createMilSymbol` uses the milsymbol library via dynamic ESM import.
|
|
16
|
+
*/
|
|
5
17
|
class OlMilitaryService {
|
|
18
|
+
idCounter = 0;
|
|
19
|
+
mlLoader = null;
|
|
20
|
+
msModule = null;
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Geometry helpers (pure math, no deps)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* Build a `Feature<Polygon>` approximating an ellipse centered at
|
|
26
|
+
* `config.center`. See {@link EllipseConfig} for parameter semantics.
|
|
27
|
+
*/
|
|
6
28
|
createEllipse(config) {
|
|
7
|
-
|
|
29
|
+
const { center, semiMajor, semiMinor, rotation = 0, segments = 64, properties } = config;
|
|
30
|
+
if (semiMajor <= 0 || semiMinor <= 0) {
|
|
31
|
+
throw new RangeError('semiMajor and semiMinor must be positive');
|
|
32
|
+
}
|
|
33
|
+
if (segments < 8) {
|
|
34
|
+
throw new RangeError('segments must be >= 8');
|
|
35
|
+
}
|
|
36
|
+
const cosR = Math.cos(rotation);
|
|
37
|
+
const sinR = Math.sin(rotation);
|
|
38
|
+
const ring = [];
|
|
39
|
+
for (let i = 0; i < segments; i++) {
|
|
40
|
+
const theta = (i / segments) * Math.PI * 2;
|
|
41
|
+
// Ellipse in local axis-aligned frame, then rotated by `rotation`.
|
|
42
|
+
const ax = Math.cos(theta) * semiMajor;
|
|
43
|
+
const ay = Math.sin(theta) * semiMinor;
|
|
44
|
+
const dx = ax * cosR - ay * sinR;
|
|
45
|
+
const dy = ax * sinR + ay * cosR;
|
|
46
|
+
ring.push(this.offsetMetersToLonLat(center, dx, dy));
|
|
47
|
+
}
|
|
48
|
+
ring.push(ring[0]); // close the ring
|
|
49
|
+
return {
|
|
50
|
+
id: this.nextId('ellipse'),
|
|
51
|
+
geometry: { type: 'Polygon', coordinates: [ring] },
|
|
52
|
+
properties,
|
|
53
|
+
};
|
|
8
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a `Feature<Polygon>` for a circular sector (pie slice).
|
|
57
|
+
* See {@link SectorConfig} for parameter semantics.
|
|
58
|
+
*/
|
|
9
59
|
createSector(config) {
|
|
10
|
-
|
|
60
|
+
const { center, radius, startAngle, endAngle, segments = 32, properties } = config;
|
|
61
|
+
if (radius <= 0) {
|
|
62
|
+
throw new RangeError('radius must be positive');
|
|
63
|
+
}
|
|
64
|
+
if (endAngle <= startAngle) {
|
|
65
|
+
throw new RangeError('endAngle must be greater than startAngle');
|
|
66
|
+
}
|
|
67
|
+
if (endAngle - startAngle > Math.PI * 2) {
|
|
68
|
+
throw new RangeError('sector cannot exceed full circle');
|
|
69
|
+
}
|
|
70
|
+
if (segments < 4) {
|
|
71
|
+
throw new RangeError('segments must be >= 4');
|
|
72
|
+
}
|
|
73
|
+
const ring = [center];
|
|
74
|
+
const span = endAngle - startAngle;
|
|
75
|
+
for (let i = 0; i <= segments; i++) {
|
|
76
|
+
const theta = startAngle + (i / segments) * span;
|
|
77
|
+
const dx = Math.cos(theta) * radius;
|
|
78
|
+
const dy = Math.sin(theta) * radius;
|
|
79
|
+
ring.push(this.offsetMetersToLonLat(center, dx, dy));
|
|
80
|
+
}
|
|
81
|
+
ring.push(center); // close back to apex
|
|
82
|
+
return {
|
|
83
|
+
id: this.nextId('sector'),
|
|
84
|
+
geometry: { type: 'Polygon', coordinates: [ring] },
|
|
85
|
+
properties,
|
|
86
|
+
};
|
|
11
87
|
}
|
|
12
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Build a `Feature<Polygon>` for a donut (annular ring). The output has
|
|
90
|
+
* two rings: an outer ring wound counter-clockwise and an inner ring
|
|
91
|
+
* wound clockwise so the GeoJSON right-hand rule renders the hole.
|
|
92
|
+
*/
|
|
93
|
+
createDonut(config) {
|
|
94
|
+
const { center, outerRadius, innerRadius, segments = 64, properties } = config;
|
|
95
|
+
if (outerRadius <= 0 || innerRadius <= 0) {
|
|
96
|
+
throw new RangeError('radii must be positive');
|
|
97
|
+
}
|
|
98
|
+
if (outerRadius <= innerRadius) {
|
|
99
|
+
throw new RangeError('outerRadius must be greater than innerRadius');
|
|
100
|
+
}
|
|
101
|
+
if (segments < 8) {
|
|
102
|
+
throw new RangeError('segments must be >= 8');
|
|
103
|
+
}
|
|
104
|
+
const outer = [];
|
|
105
|
+
const inner = [];
|
|
106
|
+
for (let i = 0; i < segments; i++) {
|
|
107
|
+
const theta = (i / segments) * Math.PI * 2;
|
|
108
|
+
const cosT = Math.cos(theta);
|
|
109
|
+
const sinT = Math.sin(theta);
|
|
110
|
+
// Outer ring: CCW (theta increasing)
|
|
111
|
+
outer.push(this.offsetMetersToLonLat(center, cosT * outerRadius, sinT * outerRadius));
|
|
112
|
+
// Inner ring: CW — sample the SAME thetas but we'll reverse the
|
|
113
|
+
// accumulator below so the ring is traversed in the opposite sense.
|
|
114
|
+
inner.push(this.offsetMetersToLonLat(center, cosT * innerRadius, sinT * innerRadius));
|
|
115
|
+
}
|
|
116
|
+
inner.reverse();
|
|
117
|
+
outer.push(outer[0]);
|
|
118
|
+
inner.push(inner[0]);
|
|
13
119
|
return {
|
|
14
|
-
id:
|
|
15
|
-
geometry: { type: '
|
|
120
|
+
id: this.nextId('donut'),
|
|
121
|
+
geometry: { type: 'Polygon', coordinates: [outer, inner] },
|
|
122
|
+
properties,
|
|
16
123
|
};
|
|
17
124
|
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// MIL-STD-2525 symbology (lazy `milsymbol` load)
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
/**
|
|
129
|
+
* Pre-load the optional `milsymbol` peer dependency so subsequent calls
|
|
130
|
+
* to `createMilSymbol` / `createMilSymbolSync` resolve immediately.
|
|
131
|
+
* Idempotent — multiple calls share the same promise.
|
|
132
|
+
*/
|
|
133
|
+
preloadMilsymbol() {
|
|
134
|
+
this.assertBrowser();
|
|
135
|
+
if (!this.mlLoader) {
|
|
136
|
+
this.mlLoader = import('milsymbol').then((m) => {
|
|
137
|
+
this.msModule = m;
|
|
138
|
+
return m;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return this.mlLoader.then(() => {
|
|
142
|
+
// Void return for the public API
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Build a MIL-STD-2525 symbol feature asynchronously.
|
|
147
|
+
* Lazy-loads `milsymbol` on the first call.
|
|
148
|
+
*/
|
|
149
|
+
async createMilSymbol(config) {
|
|
150
|
+
this.assertBrowser();
|
|
151
|
+
this.assertSidc(config.sidc);
|
|
152
|
+
if (!this.msModule) {
|
|
153
|
+
await this.preloadMilsymbol();
|
|
154
|
+
}
|
|
155
|
+
return this.buildSymbolFeature(config);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Build a MIL-STD-2525 symbol feature synchronously.
|
|
159
|
+
* Throws if `milsymbol` has not been preloaded via `preloadMilsymbol()`
|
|
160
|
+
* or a previous `createMilSymbol()` call.
|
|
161
|
+
*/
|
|
162
|
+
createMilSymbolSync(config) {
|
|
163
|
+
this.assertBrowser();
|
|
164
|
+
this.assertSidc(config.sidc);
|
|
165
|
+
if (!this.msModule) {
|
|
166
|
+
throw new Error('milsymbol is not loaded yet. Call preloadMilsymbol() or use the async createMilSymbol().');
|
|
167
|
+
}
|
|
168
|
+
return this.buildSymbolFeature(config);
|
|
169
|
+
}
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Internals
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
/**
|
|
174
|
+
* Project an `(dx, dy)` meter offset from `center` to lon/lat using a
|
|
175
|
+
* local tangent-plane (equirectangular) approximation. Acceptable for
|
|
176
|
+
* the radii typical in military symbology (<100 km from center).
|
|
177
|
+
*/
|
|
178
|
+
offsetMetersToLonLat(center, dx, dy) {
|
|
179
|
+
const [lon, lat] = center;
|
|
180
|
+
const latRad = (lat * Math.PI) / 180;
|
|
181
|
+
const dLat = dy / METERS_PER_DEGREE_LAT;
|
|
182
|
+
const dLon = dx / (METERS_PER_DEGREE_LAT * Math.cos(latRad));
|
|
183
|
+
return [lon + dLon, lat + dLat];
|
|
184
|
+
}
|
|
185
|
+
nextId(kind) {
|
|
186
|
+
return `${kind}-${++this.idCounter}`;
|
|
187
|
+
}
|
|
188
|
+
buildSymbolFeature(config) {
|
|
189
|
+
const { sidc, position, properties, quantity, ...rest } = config;
|
|
190
|
+
// `milsymbol` types `quantity` as a string, but a number is the
|
|
191
|
+
// ergonomic shape; coerce here.
|
|
192
|
+
const milOptions = {
|
|
193
|
+
...rest,
|
|
194
|
+
...(quantity !== undefined ? { quantity: String(quantity) } : {}),
|
|
195
|
+
};
|
|
196
|
+
// We asserted this.msModule exists before calling this
|
|
197
|
+
const ms = this.msModule;
|
|
198
|
+
// default import might be wrapped depending on bundler
|
|
199
|
+
const SymbolClass = ms.default?.Symbol || ms.Symbol;
|
|
200
|
+
const symbol = new SymbolClass(sidc, milOptions);
|
|
201
|
+
const style = this.symbolToStyleResult(symbol);
|
|
202
|
+
const mergedProperties = { sidc, ...milOptions, ...properties };
|
|
203
|
+
return {
|
|
204
|
+
id: this.nextId('symbol'),
|
|
205
|
+
geometry: { type: 'Point', coordinates: position },
|
|
206
|
+
properties: mergedProperties,
|
|
207
|
+
style: { icon: { src: style.src, size: style.size, anchor: style.anchor } },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
symbolToStyleResult(symbol) {
|
|
211
|
+
const svg = symbol.asSVG();
|
|
212
|
+
const { width, height } = symbol.getSize();
|
|
213
|
+
const { x: ax, y: ay } = symbol.getAnchor();
|
|
214
|
+
return {
|
|
215
|
+
src: `data:image/svg+xml;base64,${this.encodeBase64Utf8(svg)}`,
|
|
216
|
+
size: [width, height],
|
|
217
|
+
anchor: [ax / width, ay / height],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
encodeBase64Utf8(input) {
|
|
221
|
+
// `btoa` only handles Latin-1; this round-trip preserves non-ASCII
|
|
222
|
+
// characters (e.g. unit designators with accents).
|
|
223
|
+
return btoa(unescape(encodeURIComponent(input)));
|
|
224
|
+
}
|
|
225
|
+
assertSidc(sidc) {
|
|
226
|
+
if (typeof sidc !== 'string' || sidc.length < 10) {
|
|
227
|
+
throw new TypeError('sidc must be a non-empty MIL-STD-2525 SIDC string');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
assertBrowser() {
|
|
231
|
+
if (typeof window === 'undefined') {
|
|
232
|
+
throw new Error('createMilSymbol requires a browser environment');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
18
235
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlMilitaryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
19
236
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: OlMilitaryService });
|
|
20
237
|
}
|
|
@@ -2,6 +2,7 @@ import * as i0 from '@angular/core';
|
|
|
2
2
|
import { inject, ElementRef, input, output, DestroyRef, effect, ChangeDetectionStrategy, Component, Directive, EnvironmentInjector, ApplicationRef, createComponent, Injectable } from '@angular/core';
|
|
3
3
|
import Overlay from 'ol/Overlay';
|
|
4
4
|
import { OlMapService, OlZoneHelper } from '@angular-helpers/openlayers/core';
|
|
5
|
+
import { OlLayerService } from '@angular-helpers/openlayers/layers';
|
|
5
6
|
|
|
6
7
|
// OlPopupComponent — declarative popup with content projection.
|
|
7
8
|
/**
|
|
@@ -446,7 +447,7 @@ function applyOverlayConfig(overlay, config) {
|
|
|
446
447
|
}
|
|
447
448
|
|
|
448
449
|
function withOverlays() {
|
|
449
|
-
return { kind: 'overlays', providers: [OlPopupService] };
|
|
450
|
+
return { kind: 'overlays', providers: [OlLayerService, OlPopupService] };
|
|
450
451
|
}
|
|
451
452
|
function provideOverlays() {
|
|
452
453
|
return withOverlays();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-helpers/openlayers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Modern Angular wrapper for OpenLayers with modular architecture, standalone components, and hybrid template/programmatic API",
|
|
5
5
|
"homepage": "https://gaspar1992.github.io/angular-helpers/docs/openlayers",
|
|
6
6
|
"repository": {
|
|
@@ -32,9 +32,13 @@
|
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@angular/common": "^21.0.0",
|
|
34
34
|
"@angular/core": "^21.0.0",
|
|
35
|
+
"milsymbol": "^3.0.0",
|
|
35
36
|
"ol": "^10.0.0"
|
|
36
37
|
},
|
|
37
38
|
"peerDependenciesMeta": {
|
|
39
|
+
"milsymbol": {
|
|
40
|
+
"optional": true
|
|
41
|
+
},
|
|
38
42
|
"ol": {
|
|
39
43
|
"optional": false
|
|
40
44
|
}
|
|
@@ -36,6 +36,22 @@ interface Style {
|
|
|
36
36
|
width?: number;
|
|
37
37
|
};
|
|
38
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Icon style for Point features. When set, renderers should produce an
|
|
41
|
+
* `ol/style/Icon` with the given source. Used by the military symbology
|
|
42
|
+
* helpers to embed `milsymbol`-generated SVGs as data URLs.
|
|
43
|
+
*/
|
|
44
|
+
icon?: {
|
|
45
|
+
/** Image source — typically a `data:image/svg+xml;base64,...` URL */
|
|
46
|
+
src: string;
|
|
47
|
+
/** Pixel size `[width, height]` */
|
|
48
|
+
size?: [number, number];
|
|
49
|
+
/**
|
|
50
|
+
* Anchor in fractional coordinates `[x, y]` where `0..1` is the icon
|
|
51
|
+
* extent. `[0.5, 0.5]` centers the icon on the feature.
|
|
52
|
+
*/
|
|
53
|
+
anchor?: [number, number];
|
|
54
|
+
};
|
|
39
55
|
text?: {
|
|
40
56
|
text?: string;
|
|
41
57
|
font?: string;
|
|
@@ -83,6 +99,7 @@ declare class OlMapComponent {
|
|
|
83
99
|
mapDblClick: _angular_core.OutputEmitterRef<MapClickEvent>;
|
|
84
100
|
mapContainerRef: _angular_core.Signal<ElementRef<HTMLDivElement>>;
|
|
85
101
|
private map?;
|
|
102
|
+
private resizeObserver?;
|
|
86
103
|
constructor();
|
|
87
104
|
private initMap;
|
|
88
105
|
private destroyMap;
|
|
@@ -11,6 +11,12 @@ import { Feature as Feature$1 } from 'ol';
|
|
|
11
11
|
type InteractionType = 'select' | 'draw' | 'modify' | 'dragAndDrop';
|
|
12
12
|
interface InteractionConfig {
|
|
13
13
|
active?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* If true, enabling this interaction will automatically disable other active exclusive interactions.
|
|
16
|
+
* Useful for mutually exclusive tools like Draw and Modify.
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
exclusive?: boolean;
|
|
14
20
|
}
|
|
15
21
|
interface SelectConfig extends InteractionConfig {
|
|
16
22
|
layers?: string[];
|
|
@@ -178,6 +184,7 @@ declare class InteractionStateService {
|
|
|
178
184
|
readonly modify$: rxjs.Observable<ModifyEvent>;
|
|
179
185
|
/**
|
|
180
186
|
* Adds a managed interaction to the state.
|
|
187
|
+
* If the interaction is marked as exclusive, it disables other exclusive interactions.
|
|
181
188
|
* @param interaction - The interaction to add
|
|
182
189
|
*/
|
|
183
190
|
addInteraction(interaction: ManagedInteraction): void;
|
|
@@ -1,7 +1,52 @@
|
|
|
1
1
|
import * as _angular_core from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { Layer, Extent, Feature, Style, OlFeature } from '@angular-helpers/openlayers/core';
|
|
3
3
|
import BaseLayer from 'ol/layer/Base';
|
|
4
4
|
|
|
5
|
+
interface LayerConfig extends Layer {
|
|
6
|
+
type: 'vector' | 'tile' | 'image';
|
|
7
|
+
extent?: Extent;
|
|
8
|
+
minResolution?: number;
|
|
9
|
+
maxResolution?: number;
|
|
10
|
+
}
|
|
11
|
+
interface ClusterConfig {
|
|
12
|
+
/** Enable clustering (default: false) */
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
/** Distance in pixels within which features will be clustered (default: 40) */
|
|
15
|
+
distance?: number;
|
|
16
|
+
/** Minimum distance between clusters (default: 20) */
|
|
17
|
+
minDistance?: number;
|
|
18
|
+
/** Show count badge on cluster (default: true) */
|
|
19
|
+
showCount?: boolean;
|
|
20
|
+
/** Style for individual features when clustering */
|
|
21
|
+
featureStyle?: Style;
|
|
22
|
+
}
|
|
23
|
+
interface VectorLayerConfig extends LayerConfig {
|
|
24
|
+
type: 'vector';
|
|
25
|
+
features?: Feature[];
|
|
26
|
+
style?: Style | ((feature: Feature) => Style);
|
|
27
|
+
cluster?: ClusterConfig;
|
|
28
|
+
}
|
|
29
|
+
interface TileLayerConfig extends LayerConfig {
|
|
30
|
+
type: 'tile';
|
|
31
|
+
source: SourceConfig;
|
|
32
|
+
}
|
|
33
|
+
interface ImageLayerConfig extends LayerConfig {
|
|
34
|
+
type: 'image';
|
|
35
|
+
source: ImageSourceConfig;
|
|
36
|
+
}
|
|
37
|
+
interface SourceConfig {
|
|
38
|
+
type: 'osm' | 'xyz' | 'wms' | 'wmts';
|
|
39
|
+
url?: string;
|
|
40
|
+
attributions?: string | string[];
|
|
41
|
+
params?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
interface ImageSourceConfig {
|
|
44
|
+
type: 'wms' | 'static';
|
|
45
|
+
url: string;
|
|
46
|
+
params?: Record<string, unknown>;
|
|
47
|
+
imageExtent?: Extent;
|
|
48
|
+
}
|
|
49
|
+
|
|
5
50
|
declare class OlVectorLayerComponent {
|
|
6
51
|
private layerService;
|
|
7
52
|
private destroyRef;
|
|
@@ -11,9 +56,10 @@ declare class OlVectorLayerComponent {
|
|
|
11
56
|
opacity: _angular_core.InputSignal<number>;
|
|
12
57
|
visible: _angular_core.InputSignal<boolean>;
|
|
13
58
|
style: _angular_core.InputSignal<Style | ((feature: Feature) => Style)>;
|
|
59
|
+
cluster: _angular_core.InputSignal<ClusterConfig>;
|
|
14
60
|
constructor();
|
|
15
61
|
static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlVectorLayerComponent, never>;
|
|
16
|
-
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlVectorLayerComponent, "ol-vector-layer", never, { "id": { "alias": "id"; "required": true; "isSignal": true; }; "features": { "alias": "features"; "required": false; "isSignal": true; }; "zIndex": { "alias": "zIndex"; "required": false; "isSignal": true; }; "opacity": { "alias": "opacity"; "required": false; "isSignal": true; }; "visible": { "alias": "visible"; "required": false; "isSignal": true; }; "style": { "alias": "style"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
62
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlVectorLayerComponent, "ol-vector-layer", never, { "id": { "alias": "id"; "required": true; "isSignal": true; }; "features": { "alias": "features"; "required": false; "isSignal": true; }; "zIndex": { "alias": "zIndex"; "required": false; "isSignal": true; }; "opacity": { "alias": "opacity"; "required": false; "isSignal": true; }; "visible": { "alias": "visible"; "required": false; "isSignal": true; }; "style": { "alias": "style"; "required": false; "isSignal": true; }; "cluster": { "alias": "cluster"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
17
63
|
}
|
|
18
64
|
|
|
19
65
|
declare class OlTileLayerComponent {
|
|
@@ -48,38 +94,6 @@ declare class OlImageLayerComponent {
|
|
|
48
94
|
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlImageLayerComponent, "ol-image-layer", never, { "id": { "alias": "id"; "required": true; "isSignal": true; }; "sourceType": { "alias": "sourceType"; "required": true; "isSignal": true; }; "url": { "alias": "url"; "required": true; "isSignal": true; }; "params": { "alias": "params"; "required": false; "isSignal": true; }; "imageExtent": { "alias": "imageExtent"; "required": false; "isSignal": true; }; "zIndex": { "alias": "zIndex"; "required": false; "isSignal": true; }; "opacity": { "alias": "opacity"; "required": false; "isSignal": true; }; "visible": { "alias": "visible"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
49
95
|
}
|
|
50
96
|
|
|
51
|
-
interface LayerConfig extends Layer {
|
|
52
|
-
type: 'vector' | 'tile' | 'image';
|
|
53
|
-
extent?: Extent;
|
|
54
|
-
minResolution?: number;
|
|
55
|
-
maxResolution?: number;
|
|
56
|
-
}
|
|
57
|
-
interface VectorLayerConfig extends LayerConfig {
|
|
58
|
-
type: 'vector';
|
|
59
|
-
features?: Feature[];
|
|
60
|
-
style?: Style | ((feature: Feature) => Style);
|
|
61
|
-
}
|
|
62
|
-
interface TileLayerConfig extends LayerConfig {
|
|
63
|
-
type: 'tile';
|
|
64
|
-
source: SourceConfig;
|
|
65
|
-
}
|
|
66
|
-
interface ImageLayerConfig extends LayerConfig {
|
|
67
|
-
type: 'image';
|
|
68
|
-
source: ImageSourceConfig;
|
|
69
|
-
}
|
|
70
|
-
interface SourceConfig {
|
|
71
|
-
type: 'osm' | 'xyz' | 'wms' | 'wmts';
|
|
72
|
-
url?: string;
|
|
73
|
-
attributions?: string | string[];
|
|
74
|
-
params?: Record<string, unknown>;
|
|
75
|
-
}
|
|
76
|
-
interface ImageSourceConfig {
|
|
77
|
-
type: 'wms' | 'static';
|
|
78
|
-
url: string;
|
|
79
|
-
params?: Record<string, unknown>;
|
|
80
|
-
imageExtent?: Extent;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
97
|
interface LayerInfo {
|
|
84
98
|
id: string;
|
|
85
99
|
type: 'vector' | 'tile' | 'image';
|
|
@@ -1,27 +1,181 @@
|
|
|
1
1
|
import { Coordinate, Feature, OlFeature } from '@angular-helpers/openlayers/core';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Configuration for an ellipse polygon centered at `center`.
|
|
6
|
+
* Coordinates are emitted in EPSG:4326 (lon/lat) using a local tangent-plane
|
|
7
|
+
* approximation; accuracy degrades for radii > ~100 km or near the poles.
|
|
8
|
+
*/
|
|
4
9
|
interface EllipseConfig {
|
|
10
|
+
/** Ellipse center as `[lon, lat]` in EPSG:4326. */
|
|
5
11
|
center: Coordinate;
|
|
12
|
+
/** Semi-major axis in meters. Must be > 0. */
|
|
6
13
|
semiMajor: number;
|
|
14
|
+
/** Semi-minor axis in meters. Must be > 0. */
|
|
7
15
|
semiMinor: number;
|
|
16
|
+
/**
|
|
17
|
+
* Rotation in radians, counter-clockwise from East. Default: 0
|
|
18
|
+
* (semi-major axis points East).
|
|
19
|
+
*/
|
|
8
20
|
rotation?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Number of vertices used to approximate the ellipse. Default: 64.
|
|
23
|
+
* Minimum: 8.
|
|
24
|
+
*/
|
|
25
|
+
segments?: number;
|
|
26
|
+
/** Custom feature properties to attach to the output feature. */
|
|
27
|
+
properties?: Record<string, unknown>;
|
|
9
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Configuration for a circular sector (pie-slice) polygon.
|
|
31
|
+
* Same projection caveats as `EllipseConfig`.
|
|
32
|
+
*/
|
|
10
33
|
interface SectorConfig {
|
|
34
|
+
/** Sector apex / center as `[lon, lat]` in EPSG:4326. */
|
|
11
35
|
center: Coordinate;
|
|
36
|
+
/** Sector radius in meters. Must be > 0. */
|
|
12
37
|
radius: number;
|
|
38
|
+
/** Start angle in radians (0 = East, CCW positive). */
|
|
13
39
|
startAngle: number;
|
|
40
|
+
/**
|
|
41
|
+
* End angle in radians. Must satisfy `startAngle < endAngle <= startAngle + 2π`.
|
|
42
|
+
*/
|
|
14
43
|
endAngle: number;
|
|
44
|
+
/**
|
|
45
|
+
* Number of vertices along the arc. Default: 32. Minimum: 4. The output
|
|
46
|
+
* polygon has `segments + 3` vertices (apex + arc + apex closer).
|
|
47
|
+
*/
|
|
48
|
+
segments?: number;
|
|
49
|
+
/** Custom feature properties to attach to the output feature. */
|
|
50
|
+
properties?: Record<string, unknown>;
|
|
15
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Configuration for a donut (annular ring) polygon — a disk with a
|
|
54
|
+
* concentric circular hole. Useful for range rings, exclusion zones,
|
|
55
|
+
* and similar GIS military primitives.
|
|
56
|
+
*
|
|
57
|
+
* The output `Feature<Polygon>` has TWO rings: an outer ring (CCW) and
|
|
58
|
+
* an inner ring (CW per the GeoJSON right-hand rule), so renderers that
|
|
59
|
+
* follow the spec fill only the band between the radii.
|
|
60
|
+
*/
|
|
61
|
+
interface DonutConfig {
|
|
62
|
+
/** Donut center as `[lon, lat]` in EPSG:4326. */
|
|
63
|
+
center: Coordinate;
|
|
64
|
+
/** Outer radius in meters. Must be > `innerRadius`. */
|
|
65
|
+
outerRadius: number;
|
|
66
|
+
/** Inner radius in meters. Must be > 0 and < `outerRadius`. */
|
|
67
|
+
innerRadius: number;
|
|
68
|
+
/**
|
|
69
|
+
* Number of vertices per ring. Default: 64. Minimum: 8. Both rings use
|
|
70
|
+
* the same segment count.
|
|
71
|
+
*/
|
|
72
|
+
segments?: number;
|
|
73
|
+
/** Custom feature properties to attach to the output feature. */
|
|
74
|
+
properties?: Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Subset of `milsymbol`'s `SymbolOptions` exposed by this package.
|
|
78
|
+
* `sidc` is the only required field; everything else is optional and
|
|
79
|
+
* forwarded verbatim to `new Symbol(sidc, options)`.
|
|
80
|
+
*/
|
|
16
81
|
interface MilSymbolConfig {
|
|
82
|
+
/** MIL-STD-2525 SIDC code. Required. */
|
|
17
83
|
sidc: string;
|
|
84
|
+
/** Symbol position as `[lon, lat]` in EPSG:4326. Required. */
|
|
18
85
|
position: Coordinate;
|
|
86
|
+
/** Symbol size in pixels. Default (set by `milsymbol`): 30. */
|
|
87
|
+
size?: number;
|
|
88
|
+
/** Mono-color override (e.g. `'#000'`). */
|
|
89
|
+
monoColor?: string;
|
|
90
|
+
/** Outline color. */
|
|
91
|
+
outlineColor?: string;
|
|
92
|
+
/** Icon (interior glyph) color. */
|
|
93
|
+
iconColor?: string;
|
|
94
|
+
/** Additional information field (top of the symbol). */
|
|
95
|
+
additionalInformation?: string;
|
|
96
|
+
/** Staff comments field. */
|
|
97
|
+
staffComments?: string;
|
|
98
|
+
/** Quantity field. */
|
|
99
|
+
quantity?: number;
|
|
100
|
+
/** Unique designation field (unit identifier). */
|
|
101
|
+
uniqueDesignation?: string;
|
|
102
|
+
/** Custom feature properties merged into the output feature's `properties`. */
|
|
103
|
+
properties?: Record<string, unknown>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Result of resolving a `MilSymbolConfig` against `milsymbol`. Embedded
|
|
107
|
+
* into the output feature's `style.icon` so that `<ol-vector-layer>` can
|
|
108
|
+
* render it as an `ol/style/Icon`.
|
|
109
|
+
*/
|
|
110
|
+
interface MilSymbolStyleResult {
|
|
111
|
+
/** `data:image/svg+xml;base64,...` URL produced from `Symbol.asSVG()`. */
|
|
112
|
+
src: string;
|
|
113
|
+
/** Pixel `[width, height]` from `Symbol.getSize()`. */
|
|
114
|
+
size: [number, number];
|
|
115
|
+
/**
|
|
116
|
+
* Anchor in fractional coordinates `[x, y]` (0..1), computed from
|
|
117
|
+
* `Symbol.getAnchor()` divided by the size. `[0.5, 0.5]` centers the
|
|
118
|
+
* icon on the feature.
|
|
119
|
+
*/
|
|
120
|
+
anchor: [number, number];
|
|
19
121
|
}
|
|
20
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Service exposing geometry helpers and MIL-STD-2525 symbology rendering.
|
|
125
|
+
*
|
|
126
|
+
* - `createEllipse`, `createSector`, `createDonut` are **pure math** and
|
|
127
|
+
* have no runtime dependencies beyond the bundled types.
|
|
128
|
+
* - `createMilSymbol` uses the milsymbol library via dynamic ESM import.
|
|
129
|
+
*/
|
|
21
130
|
declare class OlMilitaryService {
|
|
131
|
+
private idCounter;
|
|
132
|
+
private mlLoader;
|
|
133
|
+
private msModule;
|
|
134
|
+
/**
|
|
135
|
+
* Build a `Feature<Polygon>` approximating an ellipse centered at
|
|
136
|
+
* `config.center`. See {@link EllipseConfig} for parameter semantics.
|
|
137
|
+
*/
|
|
22
138
|
createEllipse(config: EllipseConfig): Feature;
|
|
139
|
+
/**
|
|
140
|
+
* Build a `Feature<Polygon>` for a circular sector (pie slice).
|
|
141
|
+
* See {@link SectorConfig} for parameter semantics.
|
|
142
|
+
*/
|
|
23
143
|
createSector(config: SectorConfig): Feature;
|
|
24
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Build a `Feature<Polygon>` for a donut (annular ring). The output has
|
|
146
|
+
* two rings: an outer ring wound counter-clockwise and an inner ring
|
|
147
|
+
* wound clockwise so the GeoJSON right-hand rule renders the hole.
|
|
148
|
+
*/
|
|
149
|
+
createDonut(config: DonutConfig): Feature;
|
|
150
|
+
/**
|
|
151
|
+
* Pre-load the optional `milsymbol` peer dependency so subsequent calls
|
|
152
|
+
* to `createMilSymbol` / `createMilSymbolSync` resolve immediately.
|
|
153
|
+
* Idempotent — multiple calls share the same promise.
|
|
154
|
+
*/
|
|
155
|
+
preloadMilsymbol(): Promise<void>;
|
|
156
|
+
/**
|
|
157
|
+
* Build a MIL-STD-2525 symbol feature asynchronously.
|
|
158
|
+
* Lazy-loads `milsymbol` on the first call.
|
|
159
|
+
*/
|
|
160
|
+
createMilSymbol(config: MilSymbolConfig): Promise<Feature>;
|
|
161
|
+
/**
|
|
162
|
+
* Build a MIL-STD-2525 symbol feature synchronously.
|
|
163
|
+
* Throws if `milsymbol` has not been preloaded via `preloadMilsymbol()`
|
|
164
|
+
* or a previous `createMilSymbol()` call.
|
|
165
|
+
*/
|
|
166
|
+
createMilSymbolSync(config: MilSymbolConfig): Feature;
|
|
167
|
+
/**
|
|
168
|
+
* Project an `(dx, dy)` meter offset from `center` to lon/lat using a
|
|
169
|
+
* local tangent-plane (equirectangular) approximation. Acceptable for
|
|
170
|
+
* the radii typical in military symbology (<100 km from center).
|
|
171
|
+
*/
|
|
172
|
+
private offsetMetersToLonLat;
|
|
173
|
+
private nextId;
|
|
174
|
+
private buildSymbolFeature;
|
|
175
|
+
private symbolToStyleResult;
|
|
176
|
+
private encodeBase64Utf8;
|
|
177
|
+
private assertSidc;
|
|
178
|
+
private assertBrowser;
|
|
25
179
|
static ɵfac: i0.ɵɵFactoryDeclaration<OlMilitaryService, never>;
|
|
26
180
|
static ɵprov: i0.ɵɵInjectableDeclaration<OlMilitaryService>;
|
|
27
181
|
}
|
|
@@ -30,4 +184,4 @@ declare function withMilitary(): OlFeature<'military'>;
|
|
|
30
184
|
declare function provideMilitary(): OlFeature<'military'>;
|
|
31
185
|
|
|
32
186
|
export { OlMilitaryService, provideMilitary, withMilitary };
|
|
33
|
-
export type { EllipseConfig, MilSymbolConfig, SectorConfig };
|
|
187
|
+
export type { DonutConfig, EllipseConfig, MilSymbolConfig, MilSymbolStyleResult, SectorConfig };
|