@angular-helpers/openlayers 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -184,87 +184,150 @@ const handle = popups.openComponent({
184
184
 
185
185
  `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
186
 
187
- ## Military symbology
187
+ ## WebGL Layers — GPU-accelerated rendering
188
188
 
189
- Available since `0.4.0` from `@angular-helpers/openlayers/military`.
189
+ Available since `0.3.0` from `@angular-helpers/openlayers/layers`.
190
+
191
+ 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).
192
+
193
+ ### `<ol-webgl-tile-layer>` — Raster style manipulation
194
+
195
+ 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.
196
+
197
+ ```html
198
+ <ol-webgl-tile-layer
199
+ id="satellite-webgl"
200
+ source="xyz"
201
+ [url]="'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'"
202
+ [tileStyle]="{
203
+ brightness: 0.1,
204
+ contrast: 0.2,
205
+ saturation: -0.5
206
+ }"
207
+ />
208
+ ```
209
+
210
+ ### `<ol-webgl-vector-layer>` — Smooth massive datasets (10k+ features)
211
+
212
+ 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.
213
+
214
+ ```html
215
+ <ol-webgl-vector-layer
216
+ id="massive-points"
217
+ [features]="densePoints()"
218
+ [flatStyle]="{
219
+ 'circle-radius': 6,
220
+ 'circle-fill-color': '#10b981',
221
+ 'stroke-color': '#334155',
222
+ 'stroke-width': 1
223
+ }"
224
+ [disableHitDetection]="true"
225
+ />
226
+ ```
227
+
228
+ Rigorous cleanup guarantees that WebGL contexts, framebuffers, and active buffers are fully released on destroy (`layer.dispose()`), preventing GPU leaks.
229
+
230
+ ## Geodesic Geometry Helpers
190
231
 
191
- 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.
232
+ Available from `@angular-helpers/openlayers/core` via `OlGeometryService`.
233
+
234
+ 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
235
 
193
236
  ```typescript
194
- import { inject, signal } from '@angular/core';
195
- import { OlMilitaryService } from '@angular-helpers/openlayers/military';
237
+ import { inject, Component, signal } from '@angular/core';
238
+ import { OlGeometryService } from '@angular-helpers/openlayers/core';
196
239
  import type { Feature } from '@angular-helpers/openlayers/core';
197
240
 
198
241
  @Component({
199
- // …
200
242
  imports: [OlMapComponent, OlVectorLayerComponent],
201
243
  template: `
202
244
  <ol-map [center]="[2.17, 41.38]" [zoom]="8">
203
245
  <ol-tile-layer id="osm" source="osm" />
204
- <ol-vector-layer id="military" [features]="features()" [zIndex]="10" />
246
+ <ol-vector-layer id="shapes" [features]="features()" />
205
247
  </ol-map>
206
248
  `,
207
249
  })
208
- export class MilDemo {
209
- private ml = inject(OlMilitaryService);
250
+ export class GeodesicDemo {
251
+ private geomSvc = inject(OlGeometryService);
210
252
  features = signal<Feature[]>([]);
211
253
 
212
- async ngOnInit() {
213
- const ellipse = this.ml.createEllipse({
254
+ ngOnInit() {
255
+ const ellipse = this.geomSvc.createEllipse({
214
256
  center: [2.17, 41.38],
215
257
  semiMajor: 6_000,
216
258
  semiMinor: 3_000,
217
259
  rotation: Math.PI / 6,
218
260
  });
219
- const sector = this.ml.createSector({
261
+ const sector = this.geomSvc.createSector({
220
262
  center: [-0.38, 39.47],
221
263
  radius: 8_000,
222
264
  startAngle: Math.PI / 6,
223
265
  endAngle: Math.PI / 2,
224
266
  });
225
- const donut = this.ml.createDonut({
267
+ const donut = this.geomSvc.createDonut({
226
268
  center: [-5.99, 37.39],
227
269
  innerRadius: 5_000,
228
270
  outerRadius: 10_000,
229
271
  });
230
- const symbol = await this.ml.createMilSymbol({
231
- sidc: 'SFGPUCI-----',
232
- position: [-3.7, 40.42],
233
- size: 36,
234
- });
235
- this.features.set([ellipse, sector, donut, symbol]);
272
+ this.features.set([ellipse, sector, donut]);
236
273
  }
237
274
  }
238
275
  ```
239
276
 
240
- ### Geometry helpers
241
-
242
277
  | Method | Output | Notes |
243
278
  | ----------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------- |
244
279
  | `createEllipse(config)` | `Feature<Polygon>` | Optional `rotation` in radians, configurable `segments` (default 64) |
245
280
  | `createSector(config)` | `Feature<Polygon>` | Pie-slice (apex-arc-apex). `startAngle < endAngle ≤ start + 2π` |
246
281
  | `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
282
 
248
- Coordinates are emitted in `EPSG:4326` (lon/lat) using true geodesic calculations (`Vincenty`'s formulae via `ol/sphere`). This means your shapes remain mathematically accurate and visually consistent (without scale distortion) across massive global distances, fulfilling military precision requirements.
283
+ ---
249
284
 
250
- ### MIL-STD-2525 symbols
285
+ ## Military Symbology & Tactical Graphics
251
286
 
252
- `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:
287
+ Available since `0.4.0` from `@angular-helpers/openlayers/military`.
253
288
 
254
- ```bash
255
- pnpm add milsymbol
289
+ 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).
290
+
291
+ ### MIL-STD-2525 Point Symbology (`OlMilitaryService`)
292
+
293
+ Lazy-loads the heavy `milsymbol` package dynamically on demand, returning a styled `Feature<Point>` so the vector layer renders it natively.
294
+
295
+ ```typescript
296
+ import { inject, Component, signal } from '@angular/core';
297
+ import { OlMilitaryService } from '@angular-helpers/openlayers/military';
298
+ import type { Feature } from '@angular-helpers/openlayers/core';
299
+
300
+ @Component({
301
+ imports: [OlMapComponent, OlVectorLayerComponent],
302
+ providers: [OlMilitaryService],
303
+ template: `
304
+ <ol-map [center]="[-3.7, 40.42]" [zoom]="8">
305
+ <ol-vector-layer id="military" [features]="features()" />
306
+ </ol-map>
307
+ `,
308
+ })
309
+ export class MilDemo {
310
+ private milSvc = inject(OlMilitaryService);
311
+ features = signal<Feature[]>([]);
312
+
313
+ async ngOnInit() {
314
+ const symbol = await this.milSvc.createMilSymbol({
315
+ sidc: 'SFGPUCI-----', // Friendly Infantry Unit
316
+ position: [-3.7, 40.42],
317
+ size: 36,
318
+ });
319
+ this.features.set([symbol]);
320
+ }
321
+ }
256
322
  ```
257
323
 
258
- ```ts
259
- const symbol = await ml.createMilSymbol({
260
- sidc: 'SFGPUCI-----', // friendly infantry, ground unit
261
- position: [-3.7, 40.42],
262
- size: 36,
263
- uniqueDesignation: 'A1',
264
- });
324
+ Install the optional peer dependency if utilizing NATO symbology:
325
+
326
+ ```bash
327
+ pnpm add milsymbol
265
328
  ```
266
329
 
267
- Three flavors:
330
+ Three execution strategies:
268
331
 
269
332
  - **`createMilSymbol(config)`** — async; lazy-loads on first call.
270
333
  - **`createMilSymbolSync(config)`** — sync; throws if `milsymbol` is not loaded yet.
@@ -272,6 +335,28 @@ Three flavors:
272
335
 
273
336
  The service throws clearly on non-browser environments (`createMilSymbol` requires `window`).
274
337
 
338
+ ### Tactical Graphics (`OlTacticalGraphicsService`)
339
+
340
+ Builds advanced multi-point military tactical graphic features (frontlines with directional teeth, attack arrow coordinates) and provides custom styles.
341
+
342
+ ```typescript
343
+ import { OlTacticalGraphicsService } from '@angular-helpers/openlayers/military';
344
+
345
+ const tacticalSvc = inject(OlTacticalGraphicsService);
346
+
347
+ // Create a frontline graphic
348
+ const frontline = tacticalSvc.createFrontLine(
349
+ [
350
+ [2.1, 41.3],
351
+ [2.2, 41.4],
352
+ ],
353
+ 'friendly',
354
+ );
355
+
356
+ // Apply specialized style for frontline teeth
357
+ const frontlineStyle = tacticalSvc.createFrontLineStyle('#4f46e5', 'friendly');
358
+ ```
359
+
275
360
  ## Architecture
276
361
 
277
362
  ### 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
5
  import { fromLonLat, toLonLat, 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
@@ -430,6 +433,216 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
430
433
  }]
431
434
  }] });
432
435
 
436
+ /**
437
+ * Service for orchestrating time-series animations in OpenLayers.
438
+ * Exposes a reactive currentTime signal that updates outside the Angular zone
439
+ * via requestAnimationFrame, ensuring 60FPS WebGL animations without triggering
440
+ * global change detection.
441
+ */
442
+ class OlTimeService {
443
+ zoneHelper = inject(OlZoneHelper);
444
+ timeSignal = signal(Date.now(), ...(ngDevMode ? [{ debugName: "timeSignal" }] : /* istanbul ignore next */ []));
445
+ playingSignal = signal(false, ...(ngDevMode ? [{ debugName: "playingSignal" }] : /* istanbul ignore next */ []));
446
+ speedSignal = signal(1, ...(ngDevMode ? [{ debugName: "speedSignal" }] : /* istanbul ignore next */ []));
447
+ animationFrameId = null;
448
+ lastTick = 0;
449
+ currentTime = computed(() => this.timeSignal(), ...(ngDevMode ? [{ debugName: "currentTime" }] : /* istanbul ignore next */ []));
450
+ isPlaying = computed(() => this.playingSignal(), ...(ngDevMode ? [{ debugName: "isPlaying" }] : /* istanbul ignore next */ []));
451
+ speed = computed(() => this.speedSignal(), ...(ngDevMode ? [{ debugName: "speed" }] : /* istanbul ignore next */ []));
452
+ /**
453
+ * Sets the current time manually.
454
+ * @param time Epoch timestamp in milliseconds
455
+ */
456
+ setTime(time) {
457
+ this.timeSignal.set(time);
458
+ }
459
+ /**
460
+ * Sets the playback speed multiplier.
461
+ * @param speed Multiplier (e.g. 1 = real time, 60 = 1 minute per second)
462
+ */
463
+ setSpeed(speed) {
464
+ this.speedSignal.set(speed);
465
+ }
466
+ /**
467
+ * Starts the time animation loop.
468
+ */
469
+ play() {
470
+ if (this.playingSignal())
471
+ return;
472
+ this.playingSignal.set(true);
473
+ this.lastTick = performance.now();
474
+ this.zoneHelper.runOutsideAngular(() => {
475
+ const loop = (now) => {
476
+ if (!this.playingSignal())
477
+ return;
478
+ const delta = now - this.lastTick;
479
+ this.lastTick = now;
480
+ // Advance time based on delta and speed multiplier
481
+ const advance = delta * this.speedSignal();
482
+ this.timeSignal.update((t) => t + advance);
483
+ this.animationFrameId = requestAnimationFrame(loop);
484
+ };
485
+ this.animationFrameId = requestAnimationFrame(loop);
486
+ });
487
+ }
488
+ /**
489
+ * Pauses the time animation loop.
490
+ */
491
+ pause() {
492
+ this.playingSignal.set(false);
493
+ if (this.animationFrameId !== null) {
494
+ cancelAnimationFrame(this.animationFrameId);
495
+ this.animationFrameId = null;
496
+ }
497
+ }
498
+ /**
499
+ * Stops the animation and resets to a specific time.
500
+ */
501
+ stop(resetTime = Date.now()) {
502
+ this.pause();
503
+ this.setTime(resetTime);
504
+ }
505
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
506
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimeService, providedIn: 'root' });
507
+ }
508
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimeService, decorators: [{
509
+ type: Injectable,
510
+ args: [{ providedIn: 'root' }]
511
+ }] });
512
+
513
+ // Feature conversion utilities for OpenLayers interactions
514
+ /**
515
+ * Converts an OpenLayers feature to the internal Feature format.
516
+ * Handles coordinate extraction and geometry type mapping.
517
+ *
518
+ * @param olFeature - The OpenLayers feature to convert
519
+ * @returns The converted Feature with normalized structure
520
+ */
521
+ function olFeatureToFeature(olFeature) {
522
+ // Unwrap spider features
523
+ const spiderFeature = olFeature.get('spider-feature');
524
+ if (spiderFeature) {
525
+ return olFeatureToFeature(spiderFeature);
526
+ }
527
+ // Unwrap single-item clusters
528
+ const clusterFeatures = olFeature.get('features');
529
+ if (Array.isArray(clusterFeatures) && clusterFeatures.length === 1) {
530
+ return olFeatureToFeature(clusterFeatures[0]);
531
+ }
532
+ const geometry = olFeature.getGeometry();
533
+ const geomType = geometry?.getType() ?? 'Point';
534
+ // Convert coordinates based on geometry type
535
+ let coordinates;
536
+ if (geomType === 'Circle') {
537
+ // ol/geom/Circle has no getCoordinates() — use getCenter() instead
538
+ const circle = geometry;
539
+ coordinates = circle.getCenter();
540
+ }
541
+ else {
542
+ // oxlint-disable-next-line no-explicit-any
543
+ const olCoords = geometry.getCoordinates();
544
+ if (Array.isArray(olCoords) && Array.isArray(olCoords[0])) {
545
+ // Multi-coordinate structures (LineString, Polygon, etc.)
546
+ coordinates = olCoords;
547
+ }
548
+ else {
549
+ // Single point
550
+ coordinates = olCoords;
551
+ }
552
+ }
553
+ return {
554
+ id: olFeature.getId()?.toString() ?? `feature-${Math.random().toString(36).slice(2)}`,
555
+ geometry: {
556
+ type: geomType,
557
+ coordinates,
558
+ },
559
+ properties: olFeature.getProperties(),
560
+ };
561
+ }
562
+ /**
563
+ * Converts an internal Feature to an OpenLayers feature.
564
+ */
565
+ function featureToOlFeature(feature) {
566
+ const geom = feature.geometry;
567
+ let geometry;
568
+ if (!geom.coordinates) {
569
+ geometry = new Point([0, 0]);
570
+ }
571
+ else if (geom.type === 'Point') {
572
+ const coords = geom.coordinates;
573
+ geometry = new Point(fromLonLat(coords));
574
+ }
575
+ else if (geom.type === 'LineString') {
576
+ const coords = geom.coordinates.map((c) => fromLonLat(c));
577
+ geometry = new LineString(coords);
578
+ }
579
+ else if (geom.type === 'Polygon') {
580
+ const rings = geom.coordinates.map((ring) => ring.map((c) => fromLonLat(c)));
581
+ geometry = new Polygon(rings);
582
+ }
583
+ else {
584
+ geometry = new Point([0, 0]);
585
+ }
586
+ // Create OL Feature
587
+ // Note: we must avoid passing 'geometry' as a plain object to OLFeature constructor,
588
+ // so we pass the object properties without it, then set geometry explicitly.
589
+ const props = { ...feature.properties };
590
+ delete props['geometry']; // Just in case
591
+ const olFeature = new FeatureClass(props);
592
+ olFeature.setGeometry(geometry);
593
+ olFeature.setId(feature.id);
594
+ return olFeature;
595
+ }
596
+
597
+ /**
598
+ * Creates an Angular resource for fetching and decoding GeoJSON into OpenLayers Features.
599
+ * Must be called in an injection context.
600
+ *
601
+ * @param url The URL signal or string to fetch data from
602
+ * @param options Additional vector resource options
603
+ * @returns An Angular Resource containing an array of parsed Features
604
+ */
605
+ function createVectorResource(url, options) {
606
+ return resource({
607
+ loader: async ({ abortSignal }) => {
608
+ const fetchUrl = url();
609
+ if (!fetchUrl)
610
+ return [];
611
+ const cacheKey = 'ol-vector-cache-v1';
612
+ const isBrowser = typeof caches !== 'undefined';
613
+ let response;
614
+ let cache;
615
+ if (isBrowser) {
616
+ cache = await caches.open(cacheKey);
617
+ const cachedResponse = await cache.match(fetchUrl);
618
+ if (cachedResponse) {
619
+ response = cachedResponse;
620
+ }
621
+ }
622
+ if (!response) {
623
+ response = await fetch(fetchUrl, {
624
+ ...options?.fetchOptions,
625
+ signal: abortSignal,
626
+ });
627
+ if (!response.ok) {
628
+ throw new Error(`Failed to fetch vector data: ${response.statusText}`);
629
+ }
630
+ if (isBrowser && cache) {
631
+ await cache.put(fetchUrl, response.clone());
632
+ }
633
+ }
634
+ // We process the text since GeoJSON readFeatures accepts string or object.
635
+ // Doing text() might be slightly faster before passing to OL's parser.
636
+ const data = await response.text();
637
+ const format = new GeoJSON();
638
+ // We parse the GeoJSON string into OpenLayers features.
639
+ const olFeatures = format.readFeatures(data);
640
+ // Convert to our generic Feature interface expected by OlVectorLayer
641
+ return olFeatures.map((f) => olFeatureToFeature(f));
642
+ },
643
+ });
644
+ }
645
+
433
646
  // Provider functions
434
647
  function provideOpenLayers(...features) {
435
648
  return makeEnvironmentProviders([
@@ -478,4 +691,4 @@ function withProjections(proj4, definitions) {
478
691
  * Generated bundle index. Do not edit.
479
692
  */
480
693
 
481
- export { OlGeometryService, OlMapComponent, OlMapService, OlZoneHelper, provideOpenLayers, withProjections };
694
+ export { OlGeometryService, OlMapComponent, OlMapService, OlTimeService, OlZoneHelper, createVectorResource, featureToOlFeature, olFeatureToFeature, provideOpenLayers, withProjections };