@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
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ export const appConfig: ApplicationConfig = {
|
|
|
45
45
|
|
|
46
46
|
### 2. Use in your component
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
````typescript
|
|
49
49
|
// map.component.ts
|
|
50
50
|
import { Component, inject, signal } from '@angular/core';
|
|
51
51
|
import { OlMapComponent } from '@angular-helpers/openlayers/core';
|
|
@@ -103,8 +103,57 @@ export class MapComponent {
|
|
|
103
103
|
this.mapService.animateView({ center: [2.2945, 48.8584], zoom: 15, duration: 1000 });
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
## Custom Projections & Coordinate Systems
|
|
107
|
+
|
|
108
|
+
Available since `0.4.1` in `@angular-helpers/openlayers/core` and `@angular-helpers/openlayers/layers`.
|
|
109
|
+
|
|
110
|
+
When working with local reference systems (like UTM zones), you can register custom projections globally and pass coordinates in those reference systems directly to `[center]` or `[features]` without manual transforms:
|
|
111
|
+
|
|
112
|
+
### 1. Register custom projections globally
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// app.config.ts
|
|
116
|
+
import { provideOpenLayers } from '@angular-helpers/openlayers/core';
|
|
117
|
+
import { withProjections } from '@angular-helpers/openlayers/core';
|
|
118
|
+
import proj4 from 'proj4';
|
|
119
|
+
|
|
120
|
+
export const appConfig = {
|
|
121
|
+
providers: [
|
|
122
|
+
provideOpenLayers(
|
|
123
|
+
withProjections(proj4, [
|
|
124
|
+
{
|
|
125
|
+
code: 'EPSG:25830', // UTM Zone 30N
|
|
126
|
+
def: '+proj=utm +zone=30 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs',
|
|
127
|
+
extent: [0, 0, 1000000, 10000000],
|
|
128
|
+
},
|
|
129
|
+
]),
|
|
130
|
+
),
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
````
|
|
134
|
+
|
|
135
|
+
### 2. Pass local coordinates natively to the map
|
|
136
|
+
|
|
137
|
+
By setting `[coordinateProjection]` to match the custom projection, the component automatically bypasses longitude/latitude conversion, feeding UTM coordinates directly into OpenLayers:
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<ol-map
|
|
141
|
+
[projection]="'EPSG:25830'"
|
|
142
|
+
[coordinateProjection]="'EPSG:25830'"
|
|
143
|
+
[center]="[440291, 4474255]" <!-- Madrid UTM Zone 30 coordinates -->
|
|
144
|
+
[zoom]="12"
|
|
145
|
+
(viewChange)="onViewChange($event)"
|
|
146
|
+
>
|
|
147
|
+
<ol-vector-layer
|
|
148
|
+
id="shapes"
|
|
149
|
+
[features]="utmFeatures()"
|
|
150
|
+
[coordinateProjection]="'EPSG:25830'"
|
|
151
|
+
/>
|
|
152
|
+
</ol-map>
|
|
106
153
|
```
|
|
107
154
|
|
|
155
|
+
---
|
|
156
|
+
|
|
108
157
|
## Overlays — popups and tooltips
|
|
109
158
|
|
|
110
159
|
Available since `0.3.0` from `@angular-helpers/openlayers/overlays`.
|
|
@@ -184,87 +233,150 @@ const handle = popups.openComponent({
|
|
|
184
233
|
|
|
185
234
|
`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`.
|
|
186
235
|
|
|
187
|
-
##
|
|
236
|
+
## WebGL Layers — GPU-accelerated rendering
|
|
188
237
|
|
|
189
|
-
Available since `0.
|
|
238
|
+
Available since `0.3.0` from `@angular-helpers/openlayers/layers`.
|
|
239
|
+
|
|
240
|
+
WebGL layers render directly on the GPU, making them perfect for extremely heavy tile configurations (with real-time styling expressions) and massive coordinate datasets (10,000+ vector points).
|
|
190
241
|
|
|
191
|
-
|
|
242
|
+
### `<ol-webgl-tile-layer>` — Raster style manipulation
|
|
243
|
+
|
|
244
|
+
Renders tile layers (OSM, XYZ, MVT) via WebGL. Supports the dynamic application of WebGL tile styles (raster expressions) for dynamic, GPU-powered adjustments like brightness, contrast, saturation, and gamma.
|
|
245
|
+
|
|
246
|
+
```html
|
|
247
|
+
<ol-webgl-tile-layer
|
|
248
|
+
id="satellite-webgl"
|
|
249
|
+
source="xyz"
|
|
250
|
+
[url]="'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'"
|
|
251
|
+
[tileStyle]="{
|
|
252
|
+
brightness: 0.1,
|
|
253
|
+
contrast: 0.2,
|
|
254
|
+
saturation: -0.5
|
|
255
|
+
}"
|
|
256
|
+
/>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### `<ol-webgl-vector-layer>` — Smooth massive datasets (10k+ features)
|
|
260
|
+
|
|
261
|
+
Renders points, lines, and polygons using WebGL 2. For peak performance, hit detection is disabled by default, and styling must be declared using `FlatStyleLike` expressions rather than standard `ol/style/Style` instances.
|
|
262
|
+
|
|
263
|
+
```html
|
|
264
|
+
<ol-webgl-vector-layer
|
|
265
|
+
id="massive-points"
|
|
266
|
+
[features]="densePoints()"
|
|
267
|
+
[flatStyle]="{
|
|
268
|
+
'circle-radius': 6,
|
|
269
|
+
'circle-fill-color': '#10b981',
|
|
270
|
+
'stroke-color': '#334155',
|
|
271
|
+
'stroke-width': 1
|
|
272
|
+
}"
|
|
273
|
+
[disableHitDetection]="true"
|
|
274
|
+
/>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Rigorous cleanup guarantees that WebGL contexts, framebuffers, and active buffers are fully released on destroy (`layer.dispose()`), preventing GPU leaks.
|
|
278
|
+
|
|
279
|
+
## Geodesic Geometry Helpers
|
|
280
|
+
|
|
281
|
+
Available from `@angular-helpers/openlayers/core` via `OlGeometryService`.
|
|
282
|
+
|
|
283
|
+
Approximates standard shapes in metric space using true geodesic calculations (`Vincenty`'s formulae via `ol/sphere`). This means your shapes remain mathematically accurate and visually consistent (without map projection scale distortion) across massive global distances.
|
|
192
284
|
|
|
193
285
|
```typescript
|
|
194
|
-
import { inject, signal } from '@angular/core';
|
|
195
|
-
import {
|
|
286
|
+
import { inject, Component, signal } from '@angular/core';
|
|
287
|
+
import { OlGeometryService } from '@angular-helpers/openlayers/core';
|
|
196
288
|
import type { Feature } from '@angular-helpers/openlayers/core';
|
|
197
289
|
|
|
198
290
|
@Component({
|
|
199
|
-
// …
|
|
200
291
|
imports: [OlMapComponent, OlVectorLayerComponent],
|
|
201
292
|
template: `
|
|
202
293
|
<ol-map [center]="[2.17, 41.38]" [zoom]="8">
|
|
203
294
|
<ol-tile-layer id="osm" source="osm" />
|
|
204
|
-
<ol-vector-layer id="
|
|
295
|
+
<ol-vector-layer id="shapes" [features]="features()" />
|
|
205
296
|
</ol-map>
|
|
206
297
|
`,
|
|
207
298
|
})
|
|
208
|
-
export class
|
|
209
|
-
private
|
|
299
|
+
export class GeodesicDemo {
|
|
300
|
+
private geomSvc = inject(OlGeometryService);
|
|
210
301
|
features = signal<Feature[]>([]);
|
|
211
302
|
|
|
212
|
-
|
|
213
|
-
const ellipse = this.
|
|
303
|
+
ngOnInit() {
|
|
304
|
+
const ellipse = this.geomSvc.createEllipse({
|
|
214
305
|
center: [2.17, 41.38],
|
|
215
306
|
semiMajor: 6_000,
|
|
216
307
|
semiMinor: 3_000,
|
|
217
308
|
rotation: Math.PI / 6,
|
|
218
309
|
});
|
|
219
|
-
const sector = this.
|
|
310
|
+
const sector = this.geomSvc.createSector({
|
|
220
311
|
center: [-0.38, 39.47],
|
|
221
312
|
radius: 8_000,
|
|
222
313
|
startAngle: Math.PI / 6,
|
|
223
314
|
endAngle: Math.PI / 2,
|
|
224
315
|
});
|
|
225
|
-
const donut = this.
|
|
316
|
+
const donut = this.geomSvc.createDonut({
|
|
226
317
|
center: [-5.99, 37.39],
|
|
227
318
|
innerRadius: 5_000,
|
|
228
319
|
outerRadius: 10_000,
|
|
229
320
|
});
|
|
230
|
-
|
|
231
|
-
sidc: 'SFGPUCI-----',
|
|
232
|
-
position: [-3.7, 40.42],
|
|
233
|
-
size: 36,
|
|
234
|
-
});
|
|
235
|
-
this.features.set([ellipse, sector, donut, symbol]);
|
|
321
|
+
this.features.set([ellipse, sector, donut]);
|
|
236
322
|
}
|
|
237
323
|
}
|
|
238
324
|
```
|
|
239
325
|
|
|
240
|
-
### Geometry helpers
|
|
241
|
-
|
|
242
326
|
| Method | Output | Notes |
|
|
243
327
|
| ----------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
|
244
328
|
| `createEllipse(config)` | `Feature<Polygon>` | Optional `rotation` in radians, configurable `segments` (default 64) |
|
|
245
329
|
| `createSector(config)` | `Feature<Polygon>` | Pie-slice (apex-arc-apex). `startAngle < endAngle ≤ start + 2π` |
|
|
246
330
|
| `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 |
|
|
247
331
|
|
|
248
|
-
|
|
332
|
+
---
|
|
249
333
|
|
|
250
|
-
|
|
334
|
+
## Military Symbology & Tactical Graphics
|
|
251
335
|
|
|
252
|
-
|
|
336
|
+
Available since `0.4.0` from `@angular-helpers/openlayers/military`.
|
|
253
337
|
|
|
254
|
-
|
|
255
|
-
|
|
338
|
+
Exposes NATO MIL-STD-2525 symbol rendering backed by the optional [`milsymbol`](https://github.com/spatialillusions/milsymbol) peer dependency, plus tactical military graphic components (frontlines, attack vectors).
|
|
339
|
+
|
|
340
|
+
### MIL-STD-2525 Point Symbology (`OlMilitaryService`)
|
|
341
|
+
|
|
342
|
+
Lazy-loads the heavy `milsymbol` package dynamically on demand, returning a styled `Feature<Point>` so the vector layer renders it natively.
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { inject, Component, signal } from '@angular/core';
|
|
346
|
+
import { OlMilitaryService } from '@angular-helpers/openlayers/military';
|
|
347
|
+
import type { Feature } from '@angular-helpers/openlayers/core';
|
|
348
|
+
|
|
349
|
+
@Component({
|
|
350
|
+
imports: [OlMapComponent, OlVectorLayerComponent],
|
|
351
|
+
providers: [OlMilitaryService],
|
|
352
|
+
template: `
|
|
353
|
+
<ol-map [center]="[-3.7, 40.42]" [zoom]="8">
|
|
354
|
+
<ol-vector-layer id="military" [features]="features()" />
|
|
355
|
+
</ol-map>
|
|
356
|
+
`,
|
|
357
|
+
})
|
|
358
|
+
export class MilDemo {
|
|
359
|
+
private milSvc = inject(OlMilitaryService);
|
|
360
|
+
features = signal<Feature[]>([]);
|
|
361
|
+
|
|
362
|
+
async ngOnInit() {
|
|
363
|
+
const symbol = await this.milSvc.createMilSymbol({
|
|
364
|
+
sidc: 'SFGPUCI-----', // Friendly Infantry Unit
|
|
365
|
+
position: [-3.7, 40.42],
|
|
366
|
+
size: 36,
|
|
367
|
+
});
|
|
368
|
+
this.features.set([symbol]);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
256
371
|
```
|
|
257
372
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
size: 36,
|
|
263
|
-
uniqueDesignation: 'A1',
|
|
264
|
-
});
|
|
373
|
+
Install the optional peer dependency if utilizing NATO symbology:
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
pnpm add milsymbol
|
|
265
377
|
```
|
|
266
378
|
|
|
267
|
-
Three
|
|
379
|
+
Three execution strategies:
|
|
268
380
|
|
|
269
381
|
- **`createMilSymbol(config)`** — async; lazy-loads on first call.
|
|
270
382
|
- **`createMilSymbolSync(config)`** — sync; throws if `milsymbol` is not loaded yet.
|
|
@@ -272,6 +384,28 @@ Three flavors:
|
|
|
272
384
|
|
|
273
385
|
The service throws clearly on non-browser environments (`createMilSymbol` requires `window`).
|
|
274
386
|
|
|
387
|
+
### Tactical Graphics (`OlTacticalGraphicsService`)
|
|
388
|
+
|
|
389
|
+
Builds advanced multi-point military tactical graphic features (frontlines with directional teeth, attack arrow coordinates) and provides custom styles.
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
import { OlTacticalGraphicsService } from '@angular-helpers/openlayers/military';
|
|
393
|
+
|
|
394
|
+
const tacticalSvc = inject(OlTacticalGraphicsService);
|
|
395
|
+
|
|
396
|
+
// Create a frontline graphic
|
|
397
|
+
const frontline = tacticalSvc.createFrontLine(
|
|
398
|
+
[
|
|
399
|
+
[2.1, 41.3],
|
|
400
|
+
[2.2, 41.4],
|
|
401
|
+
],
|
|
402
|
+
'friendly',
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
// Apply specialized style for frontline teeth
|
|
406
|
+
const frontlineStyle = tacticalSvc.createFrontLineStyle('#4f46e5', 'friendly');
|
|
407
|
+
```
|
|
408
|
+
|
|
275
409
|
## Architecture
|
|
276
410
|
|
|
277
411
|
### Data vs UI Separation
|
package/core/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @angular-helpers/openlayers/core
|
|
2
|
+
|
|
3
|
+
Core library for Angular bindings to OpenLayers.
|
|
4
|
+
Provides essential models, services, and the base `<ol-map>` component.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
\`\`\`bash
|
|
9
|
+
npm install @angular-helpers/openlayers
|
|
10
|
+
\`\`\`
|
|
11
|
+
|
|
12
|
+
## Core Services & Components
|
|
13
|
+
|
|
14
|
+
- \`OlMapComponent\`: The root map component that manages the core OpenLayers `Map` instance.
|
|
15
|
+
- \`OlMapService\`: A service to retrieve and manage the map instance across child components.
|
|
16
|
+
- \`OlLayerService\`: Manages map layers dynamically.
|
|
17
|
+
- \`OlZoneHelper\`: Optimizes Angular change detection around OpenLayers events.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
\`\`\`typescript
|
|
22
|
+
import { Component } from '@angular/core';
|
|
23
|
+
import { OlMapComponent } from '@angular-helpers/openlayers';
|
|
24
|
+
import { OlTileLayerComponent } from '@angular-helpers/openlayers/layers';
|
|
25
|
+
|
|
26
|
+
@Component({
|
|
27
|
+
selector: 'app-map-demo',
|
|
28
|
+
imports: [OlMapComponent, OlTileLayerComponent],
|
|
29
|
+
template: \`
|
|
30
|
+
<ol-map [center]="[0, 0]" [zoom]="4" class="map-container">
|
|
31
|
+
<ol-tile-layer source="osm"></ol-tile-layer>
|
|
32
|
+
</ol-map>
|
|
33
|
+
\`,
|
|
34
|
+
styles: [\`
|
|
35
|
+
.map-container {
|
|
36
|
+
width: 100%;
|
|
37
|
+
height: 400px;
|
|
38
|
+
display: block;
|
|
39
|
+
}
|
|
40
|
+
\`]
|
|
41
|
+
})
|
|
42
|
+
export class MapDemoComponent {}
|
|
43
|
+
\`\`\`
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, NgZone, Injectable, signal, DestroyRef, input, output, viewChild, afterNextRender, effect, ChangeDetectionStrategy, Component, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER } from '@angular/core';
|
|
2
|
+
import { inject, NgZone, Injectable, signal, DestroyRef, input, output, viewChild, afterNextRender, effect, ChangeDetectionStrategy, Component, computed, resource, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER } from '@angular/core';
|
|
3
3
|
import OLMap from 'ol/Map';
|
|
4
4
|
import View from 'ol/View';
|
|
5
|
-
import {
|
|
5
|
+
import { transform, get } from 'ol/proj';
|
|
6
6
|
import { offset } from 'ol/sphere';
|
|
7
|
+
import GeoJSON from 'ol/format/GeoJSON';
|
|
8
|
+
import FeatureClass from 'ol/Feature';
|
|
9
|
+
import { Point, LineString, Polygon } from 'ol/geom';
|
|
7
10
|
import { register } from 'ol/proj/proj4';
|
|
8
11
|
|
|
9
12
|
// ZoneHelperService - Handles NgZone compatibility for zoneless mode
|
|
@@ -161,6 +164,7 @@ class OlMapComponent {
|
|
|
161
164
|
zoom = input(0, ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
162
165
|
rotation = input(0, ...(ngDevMode ? [{ debugName: "rotation" }] : /* istanbul ignore next */ []));
|
|
163
166
|
projection = input('EPSG:3857', ...(ngDevMode ? [{ debugName: "projection" }] : /* istanbul ignore next */ []));
|
|
167
|
+
coordinateProjection = input('EPSG:4326', ...(ngDevMode ? [{ debugName: "coordinateProjection" }] : /* istanbul ignore next */ [])); // Dynamic input for coordinate systems
|
|
164
168
|
viewChange = output();
|
|
165
169
|
mapClick = output();
|
|
166
170
|
mapDblClick = output();
|
|
@@ -187,11 +191,25 @@ class OlMapComponent {
|
|
|
187
191
|
// Cleanup when component is destroyed
|
|
188
192
|
this.destroyRef.onDestroy(() => this.destroyMap());
|
|
189
193
|
}
|
|
194
|
+
getProjectedCoordinate(coord) {
|
|
195
|
+
const coordProj = this.coordinateProjection();
|
|
196
|
+
const mapProj = this.projection();
|
|
197
|
+
if (coordProj === mapProj)
|
|
198
|
+
return coord;
|
|
199
|
+
return transform(coord, coordProj, mapProj);
|
|
200
|
+
}
|
|
201
|
+
getExternalCoordinate(coord) {
|
|
202
|
+
const coordProj = this.coordinateProjection();
|
|
203
|
+
const mapProj = this.projection();
|
|
204
|
+
if (coordProj === mapProj)
|
|
205
|
+
return coord;
|
|
206
|
+
return transform(coord, mapProj, coordProj);
|
|
207
|
+
}
|
|
190
208
|
initMap() {
|
|
191
209
|
const container = this.mapContainerRef().nativeElement;
|
|
192
210
|
this.zoneHelper.runOutsideAngular(() => {
|
|
193
211
|
const view = new View({
|
|
194
|
-
center:
|
|
212
|
+
center: this.getProjectedCoordinate(this.center()),
|
|
195
213
|
zoom: this.zoom(),
|
|
196
214
|
rotation: this.rotation(),
|
|
197
215
|
projection: this.projection(),
|
|
@@ -219,11 +237,11 @@ class OlMapComponent {
|
|
|
219
237
|
});
|
|
220
238
|
});
|
|
221
239
|
this.map.on('click', (e) => this.zoneHelper.runInsideAngular(() => this.mapClick.emit({
|
|
222
|
-
coordinate:
|
|
240
|
+
coordinate: this.getExternalCoordinate(e.coordinate),
|
|
223
241
|
pixel: e.pixel,
|
|
224
242
|
})));
|
|
225
243
|
this.map.on('dblclick', (e) => this.zoneHelper.runInsideAngular(() => this.mapDblClick.emit({
|
|
226
|
-
coordinate:
|
|
244
|
+
coordinate: this.getExternalCoordinate(e.coordinate),
|
|
227
245
|
pixel: e.pixel,
|
|
228
246
|
})));
|
|
229
247
|
});
|
|
@@ -247,7 +265,7 @@ class OlMapComponent {
|
|
|
247
265
|
if (!this.map)
|
|
248
266
|
return;
|
|
249
267
|
const view = this.map.getView();
|
|
250
|
-
const projectedCenter =
|
|
268
|
+
const projectedCenter = this.getProjectedCoordinate(center);
|
|
251
269
|
const currentCenter = view.getCenter();
|
|
252
270
|
// Only update if center is significantly different (prevents interfering with animations)
|
|
253
271
|
if (!currentCenter ||
|
|
@@ -280,23 +298,23 @@ class OlMapComponent {
|
|
|
280
298
|
const view = this.map?.getView();
|
|
281
299
|
if (view) {
|
|
282
300
|
const projectedCenter = view.getCenter() ?? [0, 0];
|
|
283
|
-
const
|
|
301
|
+
const externalCenter = this.getExternalCoordinate(projectedCenter);
|
|
284
302
|
this.viewChange.emit({
|
|
285
|
-
center:
|
|
303
|
+
center: externalCenter,
|
|
286
304
|
zoom: view.getZoom() ?? 0,
|
|
287
305
|
rotation: view.getRotation() ?? 0,
|
|
288
306
|
});
|
|
289
307
|
}
|
|
290
308
|
}
|
|
291
309
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
292
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.13", type: OlMapComponent, isStandalone: true, selector: "ol-map", inputs: { center: { classPropertyName: "center", publicName: "center", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, rotation: { classPropertyName: "rotation", publicName: "rotation", isSignal: true, isRequired: false, transformFunction: null }, projection: { classPropertyName: "projection", publicName: "projection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewChange: "viewChange", mapClick: "mapClick", mapDblClick: "mapDblClick" }, viewQueries: [{ propertyName: "mapContainerRef", first: true, predicate: ["mapContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `<div class="ol-map-container" #mapContainer></div>
|
|
310
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.13", type: OlMapComponent, isStandalone: true, selector: "ol-map", inputs: { center: { classPropertyName: "center", publicName: "center", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, rotation: { classPropertyName: "rotation", publicName: "rotation", isSignal: true, isRequired: false, transformFunction: null }, projection: { classPropertyName: "projection", publicName: "projection", isSignal: true, isRequired: false, transformFunction: null }, coordinateProjection: { classPropertyName: "coordinateProjection", publicName: "coordinateProjection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewChange: "viewChange", mapClick: "mapClick", mapDblClick: "mapDblClick" }, viewQueries: [{ propertyName: "mapContainerRef", first: true, predicate: ["mapContainer"], descendants: true, isSignal: true }], ngImport: i0, template: `<div class="ol-map-container" #mapContainer></div>
|
|
293
311
|
<ng-content />`, isInline: true, styles: [":host{display:block;width:100%;height:100%;position:relative}.ol-map-container{width:100%;height:100%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
294
312
|
}
|
|
295
313
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapComponent, decorators: [{
|
|
296
314
|
type: Component,
|
|
297
315
|
args: [{ selector: 'ol-map', template: `<div class="ol-map-container" #mapContainer></div>
|
|
298
316
|
<ng-content />`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%;height:100%;position:relative}.ol-map-container{width:100%;height:100%}\n"] }]
|
|
299
|
-
}], ctorParameters: () => [], propDecorators: { center: [{ type: i0.Input, args: [{ isSignal: true, alias: "center", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], rotation: [{ type: i0.Input, args: [{ isSignal: true, alias: "rotation", required: false }] }], projection: [{ type: i0.Input, args: [{ isSignal: true, alias: "projection", required: false }] }], viewChange: [{ type: i0.Output, args: ["viewChange"] }], mapClick: [{ type: i0.Output, args: ["mapClick"] }], mapDblClick: [{ type: i0.Output, args: ["mapDblClick"] }], mapContainerRef: [{ type: i0.ViewChild, args: ['mapContainer', { isSignal: true }] }] } });
|
|
317
|
+
}], ctorParameters: () => [], propDecorators: { center: [{ type: i0.Input, args: [{ isSignal: true, alias: "center", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], rotation: [{ type: i0.Input, args: [{ isSignal: true, alias: "rotation", required: false }] }], projection: [{ type: i0.Input, args: [{ isSignal: true, alias: "projection", required: false }] }], coordinateProjection: [{ type: i0.Input, args: [{ isSignal: true, alias: "coordinateProjection", required: false }] }], viewChange: [{ type: i0.Output, args: ["viewChange"] }], mapClick: [{ type: i0.Output, args: ["mapClick"] }], mapDblClick: [{ type: i0.Output, args: ["mapDblClick"] }], mapContainerRef: [{ type: i0.ViewChild, args: ['mapContainer', { isSignal: true }] }] } });
|
|
300
318
|
|
|
301
319
|
// OlGeometryService — general purpose geometry helpers
|
|
302
320
|
/**
|
|
@@ -430,6 +448,224 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
|
|
|
430
448
|
}]
|
|
431
449
|
}] });
|
|
432
450
|
|
|
451
|
+
/**
|
|
452
|
+
* Service for orchestrating time-series animations in OpenLayers.
|
|
453
|
+
* Exposes a reactive currentTime signal that updates outside the Angular zone
|
|
454
|
+
* via requestAnimationFrame, ensuring 60FPS WebGL animations without triggering
|
|
455
|
+
* global change detection.
|
|
456
|
+
*/
|
|
457
|
+
class OlTimeService {
|
|
458
|
+
zoneHelper = inject(OlZoneHelper);
|
|
459
|
+
timeSignal = signal(Date.now(), ...(ngDevMode ? [{ debugName: "timeSignal" }] : /* istanbul ignore next */ []));
|
|
460
|
+
playingSignal = signal(false, ...(ngDevMode ? [{ debugName: "playingSignal" }] : /* istanbul ignore next */ []));
|
|
461
|
+
speedSignal = signal(1, ...(ngDevMode ? [{ debugName: "speedSignal" }] : /* istanbul ignore next */ []));
|
|
462
|
+
animationFrameId = null;
|
|
463
|
+
lastTick = 0;
|
|
464
|
+
currentTime = computed(() => this.timeSignal(), ...(ngDevMode ? [{ debugName: "currentTime" }] : /* istanbul ignore next */ []));
|
|
465
|
+
isPlaying = computed(() => this.playingSignal(), ...(ngDevMode ? [{ debugName: "isPlaying" }] : /* istanbul ignore next */ []));
|
|
466
|
+
speed = computed(() => this.speedSignal(), ...(ngDevMode ? [{ debugName: "speed" }] : /* istanbul ignore next */ []));
|
|
467
|
+
/**
|
|
468
|
+
* Sets the current time manually.
|
|
469
|
+
* @param time Epoch timestamp in milliseconds
|
|
470
|
+
*/
|
|
471
|
+
setTime(time) {
|
|
472
|
+
this.timeSignal.set(time);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Sets the playback speed multiplier.
|
|
476
|
+
* @param speed Multiplier (e.g. 1 = real time, 60 = 1 minute per second)
|
|
477
|
+
*/
|
|
478
|
+
setSpeed(speed) {
|
|
479
|
+
this.speedSignal.set(speed);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Starts the time animation loop.
|
|
483
|
+
*/
|
|
484
|
+
play() {
|
|
485
|
+
if (this.playingSignal())
|
|
486
|
+
return;
|
|
487
|
+
this.playingSignal.set(true);
|
|
488
|
+
this.lastTick = performance.now();
|
|
489
|
+
this.zoneHelper.runOutsideAngular(() => {
|
|
490
|
+
const loop = (now) => {
|
|
491
|
+
if (!this.playingSignal())
|
|
492
|
+
return;
|
|
493
|
+
const delta = now - this.lastTick;
|
|
494
|
+
this.lastTick = now;
|
|
495
|
+
// Advance time based on delta and speed multiplier
|
|
496
|
+
const advance = delta * this.speedSignal();
|
|
497
|
+
this.timeSignal.update((t) => t + advance);
|
|
498
|
+
this.animationFrameId = requestAnimationFrame(loop);
|
|
499
|
+
};
|
|
500
|
+
this.animationFrameId = requestAnimationFrame(loop);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Pauses the time animation loop.
|
|
505
|
+
*/
|
|
506
|
+
pause() {
|
|
507
|
+
this.playingSignal.set(false);
|
|
508
|
+
if (this.animationFrameId !== null) {
|
|
509
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
510
|
+
this.animationFrameId = null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Stops the animation and resets to a specific time.
|
|
515
|
+
*/
|
|
516
|
+
stop(resetTime = Date.now()) {
|
|
517
|
+
this.pause();
|
|
518
|
+
this.setTime(resetTime);
|
|
519
|
+
}
|
|
520
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
521
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimeService, providedIn: 'root' });
|
|
522
|
+
}
|
|
523
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimeService, decorators: [{
|
|
524
|
+
type: Injectable,
|
|
525
|
+
args: [{ providedIn: 'root' }]
|
|
526
|
+
}] });
|
|
527
|
+
|
|
528
|
+
// Feature conversion utilities for OpenLayers interactions
|
|
529
|
+
function transformCoords(coords, sourceProj, targetProj) {
|
|
530
|
+
if (!sourceProj || !targetProj || sourceProj === targetProj)
|
|
531
|
+
return coords;
|
|
532
|
+
if (!Array.isArray(coords))
|
|
533
|
+
return coords;
|
|
534
|
+
if (typeof coords[0] === 'number') {
|
|
535
|
+
return transform(coords, sourceProj, targetProj);
|
|
536
|
+
}
|
|
537
|
+
return coords.map((c) => transformCoords(c, sourceProj, targetProj));
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Converts an OpenLayers feature to the internal Feature format.
|
|
541
|
+
* Handles coordinate extraction and geometry type mapping with custom projections.
|
|
542
|
+
*
|
|
543
|
+
* @param olFeature - The OpenLayers feature to convert
|
|
544
|
+
* @param options - Projection source and target codes
|
|
545
|
+
* @returns The converted Feature with normalized structure
|
|
546
|
+
*/
|
|
547
|
+
function olFeatureToFeature(olFeature, options) {
|
|
548
|
+
// Unwrap spider features
|
|
549
|
+
const spiderFeature = olFeature.get('spider-feature');
|
|
550
|
+
if (spiderFeature) {
|
|
551
|
+
return olFeatureToFeature(spiderFeature, options);
|
|
552
|
+
}
|
|
553
|
+
// Unwrap single-item clusters
|
|
554
|
+
const clusterFeatures = olFeature.get('features');
|
|
555
|
+
if (Array.isArray(clusterFeatures) && clusterFeatures.length === 1) {
|
|
556
|
+
return olFeatureToFeature(clusterFeatures[0], options);
|
|
557
|
+
}
|
|
558
|
+
const geometry = olFeature.getGeometry();
|
|
559
|
+
const geomType = geometry?.getType() ?? 'Point';
|
|
560
|
+
const sourceProj = options?.targetProjection;
|
|
561
|
+
const targetProj = options?.sourceProjection;
|
|
562
|
+
// Convert coordinates based on geometry type
|
|
563
|
+
let coordinates;
|
|
564
|
+
if (geomType === 'Circle') {
|
|
565
|
+
// ol/geom/Circle has no getCoordinates() — use getCenter() instead
|
|
566
|
+
const circle = geometry;
|
|
567
|
+
coordinates = transformCoords(circle.getCenter(), sourceProj, targetProj);
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
// oxlint-disable-next-line no-explicit-any
|
|
571
|
+
const olCoords = geometry.getCoordinates();
|
|
572
|
+
coordinates = transformCoords(olCoords, sourceProj, targetProj);
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
id: olFeature.getId()?.toString() ?? `feature-${Math.random().toString(36).slice(2)}`,
|
|
576
|
+
geometry: {
|
|
577
|
+
type: geomType,
|
|
578
|
+
coordinates,
|
|
579
|
+
},
|
|
580
|
+
properties: olFeature.getProperties(),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Converts an internal Feature to an OpenLayers feature.
|
|
585
|
+
*/
|
|
586
|
+
function featureToOlFeature(feature, options) {
|
|
587
|
+
const sourceProj = options?.sourceProjection ?? 'EPSG:4326';
|
|
588
|
+
const targetProj = options?.targetProjection ?? 'EPSG:3857';
|
|
589
|
+
const geom = feature.geometry;
|
|
590
|
+
let geometry;
|
|
591
|
+
if (!geom.coordinates) {
|
|
592
|
+
geometry = new Point([0, 0]);
|
|
593
|
+
}
|
|
594
|
+
else if (geom.type === 'Point') {
|
|
595
|
+
const coords = geom.coordinates;
|
|
596
|
+
geometry = new Point(transformCoords(coords, sourceProj, targetProj));
|
|
597
|
+
}
|
|
598
|
+
else if (geom.type === 'LineString') {
|
|
599
|
+
const coords = transformCoords(geom.coordinates, sourceProj, targetProj);
|
|
600
|
+
geometry = new LineString(coords);
|
|
601
|
+
}
|
|
602
|
+
else if (geom.type === 'Polygon') {
|
|
603
|
+
const rings = transformCoords(geom.coordinates, sourceProj, targetProj);
|
|
604
|
+
geometry = new Polygon(rings);
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
geometry = new Point([0, 0]);
|
|
608
|
+
}
|
|
609
|
+
// Create OL Feature
|
|
610
|
+
// Note: we must avoid passing 'geometry' as a plain object to OLFeature constructor,
|
|
611
|
+
// so we pass the object properties without it, then set geometry explicitly.
|
|
612
|
+
const props = { ...feature.properties };
|
|
613
|
+
delete props['geometry']; // Just in case
|
|
614
|
+
const olFeature = new FeatureClass(props);
|
|
615
|
+
olFeature.setGeometry(geometry);
|
|
616
|
+
olFeature.setId(feature.id);
|
|
617
|
+
return olFeature;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Creates an Angular resource for fetching and decoding GeoJSON into OpenLayers Features.
|
|
622
|
+
* Must be called in an injection context.
|
|
623
|
+
*
|
|
624
|
+
* @param url The URL signal or string to fetch data from
|
|
625
|
+
* @param options Additional vector resource options
|
|
626
|
+
* @returns An Angular Resource containing an array of parsed Features
|
|
627
|
+
*/
|
|
628
|
+
function createVectorResource(url, options) {
|
|
629
|
+
return resource({
|
|
630
|
+
loader: async ({ abortSignal }) => {
|
|
631
|
+
const fetchUrl = url();
|
|
632
|
+
if (!fetchUrl)
|
|
633
|
+
return [];
|
|
634
|
+
const cacheKey = 'ol-vector-cache-v1';
|
|
635
|
+
const isBrowser = typeof caches !== 'undefined';
|
|
636
|
+
let response;
|
|
637
|
+
let cache;
|
|
638
|
+
if (isBrowser) {
|
|
639
|
+
cache = await caches.open(cacheKey);
|
|
640
|
+
const cachedResponse = await cache.match(fetchUrl);
|
|
641
|
+
if (cachedResponse) {
|
|
642
|
+
response = cachedResponse;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (!response) {
|
|
646
|
+
response = await fetch(fetchUrl, {
|
|
647
|
+
...options?.fetchOptions,
|
|
648
|
+
signal: abortSignal,
|
|
649
|
+
});
|
|
650
|
+
if (!response.ok) {
|
|
651
|
+
throw new Error(`Failed to fetch vector data: ${response.statusText}`);
|
|
652
|
+
}
|
|
653
|
+
if (isBrowser && cache) {
|
|
654
|
+
await cache.put(fetchUrl, response.clone());
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// We process the text since GeoJSON readFeatures accepts string or object.
|
|
658
|
+
// Doing text() might be slightly faster before passing to OL's parser.
|
|
659
|
+
const data = await response.text();
|
|
660
|
+
const format = new GeoJSON();
|
|
661
|
+
// We parse the GeoJSON string into OpenLayers features.
|
|
662
|
+
const olFeatures = format.readFeatures(data);
|
|
663
|
+
// Convert to our generic Feature interface expected by OlVectorLayer
|
|
664
|
+
return olFeatures.map((f) => olFeatureToFeature(f));
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
433
669
|
// Provider functions
|
|
434
670
|
function provideOpenLayers(...features) {
|
|
435
671
|
return makeEnvironmentProviders([
|
|
@@ -478,4 +714,4 @@ function withProjections(proj4, definitions) {
|
|
|
478
714
|
* Generated bundle index. Do not edit.
|
|
479
715
|
*/
|
|
480
716
|
|
|
481
|
-
export { OlGeometryService, OlMapComponent, OlMapService, OlZoneHelper, provideOpenLayers, withProjections };
|
|
717
|
+
export { OlGeometryService, OlMapComponent, OlMapService, OlTimeService, OlZoneHelper, createVectorResource, featureToOlFeature, olFeatureToFeature, provideOpenLayers, withProjections };
|