@angular-helpers/openlayers 0.4.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 +131 -38
- package/core/README.md +43 -0
- package/fesm2022/angular-helpers-openlayers-controls.mjs +289 -36
- package/fesm2022/angular-helpers-openlayers-core.mjs +410 -16
- package/fesm2022/angular-helpers-openlayers-interactions.mjs +529 -71
- package/fesm2022/angular-helpers-openlayers-layers.mjs +947 -311
- package/fesm2022/angular-helpers-openlayers-military.mjs +244 -144
- package/fesm2022/angular-helpers-openlayers-overlays.mjs +35 -11
- package/package.json +2 -2
- package/types/angular-helpers-openlayers-controls.d.ts +24 -4
- package/types/angular-helpers-openlayers-core.d.ts +197 -6
- package/types/angular-helpers-openlayers-interactions.d.ts +125 -31
- package/types/angular-helpers-openlayers-layers.d.ts +166 -21
- package/types/angular-helpers-openlayers-military.d.ts +84 -94
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, NgZone, Injectable, DestroyRef, input, output, viewChild, afterNextRender, effect, ChangeDetectionStrategy, Component, makeEnvironmentProviders } 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 { fromLonLat, toLonLat } from 'ol/proj';
|
|
5
|
+
import { fromLonLat, toLonLat, get } from 'ol/proj';
|
|
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';
|
|
10
|
+
import { register } from 'ol/proj/proj4';
|
|
6
11
|
|
|
7
12
|
// ZoneHelperService - Handles NgZone compatibility for zoneless mode
|
|
8
13
|
/**
|
|
@@ -52,10 +57,10 @@ class OlZoneHelper {
|
|
|
52
57
|
}
|
|
53
58
|
return fn();
|
|
54
59
|
}
|
|
55
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
56
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
60
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlZoneHelper, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
61
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlZoneHelper });
|
|
57
62
|
}
|
|
58
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
63
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlZoneHelper, decorators: [{
|
|
59
64
|
type: Injectable
|
|
60
65
|
}] });
|
|
61
66
|
|
|
@@ -64,13 +69,22 @@ class OlMapService {
|
|
|
64
69
|
zoneHelper = inject(OlZoneHelper);
|
|
65
70
|
map = null;
|
|
66
71
|
readyCallbacks = [];
|
|
72
|
+
_resolution = signal(1, ...(ngDevMode ? [{ debugName: "_resolution" }] : /* istanbul ignore next */ []));
|
|
73
|
+
/** Signal that emits the current map resolution in meters per pixel */
|
|
74
|
+
resolution = this._resolution.asReadonly();
|
|
67
75
|
setMap(map) {
|
|
68
76
|
this.map = map;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
if (map) {
|
|
78
|
+
this._resolution.set(map.getView()?.getResolution() ?? 1);
|
|
79
|
+
const callbacks = this.readyCallbacks.splice(0);
|
|
80
|
+
for (const cb of callbacks) {
|
|
81
|
+
cb(map);
|
|
82
|
+
}
|
|
72
83
|
}
|
|
73
84
|
}
|
|
85
|
+
setResolution(resolution) {
|
|
86
|
+
this._resolution.set(resolution);
|
|
87
|
+
}
|
|
74
88
|
getMap() {
|
|
75
89
|
return this.map;
|
|
76
90
|
}
|
|
@@ -134,10 +148,10 @@ class OlMapService {
|
|
|
134
148
|
rotation: view.getRotation() ?? 0,
|
|
135
149
|
};
|
|
136
150
|
}
|
|
137
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
138
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
151
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
152
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapService });
|
|
139
153
|
}
|
|
140
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
154
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapService, decorators: [{
|
|
141
155
|
type: Injectable
|
|
142
156
|
}] });
|
|
143
157
|
|
|
@@ -201,7 +215,12 @@ class OlMapComponent {
|
|
|
201
215
|
this.resizeObserver.observe(container);
|
|
202
216
|
}
|
|
203
217
|
view.on('change:center', () => this.zoneHelper.runInsideAngular(() => this.emitViewChange()));
|
|
204
|
-
view.on('change:resolution', () =>
|
|
218
|
+
view.on('change:resolution', () => {
|
|
219
|
+
this.zoneHelper.runInsideAngular(() => {
|
|
220
|
+
this.mapService.setResolution(view.getResolution() ?? 1);
|
|
221
|
+
this.emitViewChange();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
205
224
|
this.map.on('click', (e) => this.zoneHelper.runInsideAngular(() => this.mapClick.emit({
|
|
206
225
|
coordinate: toLonLat(e.coordinate, this.projection()),
|
|
207
226
|
pixel: e.pixel,
|
|
@@ -272,16 +291,358 @@ class OlMapComponent {
|
|
|
272
291
|
});
|
|
273
292
|
}
|
|
274
293
|
}
|
|
275
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
276
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.
|
|
294
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
295
|
+
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>
|
|
277
296
|
<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 });
|
|
278
297
|
}
|
|
279
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
298
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapComponent, decorators: [{
|
|
280
299
|
type: Component,
|
|
281
300
|
args: [{ selector: 'ol-map', template: `<div class="ol-map-container" #mapContainer></div>
|
|
282
301
|
<ng-content />`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%;height:100%;position:relative}.ol-map-container{width:100%;height:100%}\n"] }]
|
|
283
302
|
}], 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 }] }] } });
|
|
284
303
|
|
|
304
|
+
// OlGeometryService — general purpose geometry helpers
|
|
305
|
+
/**
|
|
306
|
+
* Service exposing general purpose geometry helpers for creating
|
|
307
|
+
* approximated polygons (ellipses, sectors, donuts) from metric parameters.
|
|
308
|
+
*/
|
|
309
|
+
class OlGeometryService {
|
|
310
|
+
idCounter = 0;
|
|
311
|
+
/**
|
|
312
|
+
* Build a `Feature<Polygon>` approximating an ellipse centered at
|
|
313
|
+
* `config.center`. See {@link EllipseConfig} for parameter semantics.
|
|
314
|
+
*/
|
|
315
|
+
createEllipse(config) {
|
|
316
|
+
const { center, semiMajor, semiMinor, rotation = 0, segments = 64, properties } = config;
|
|
317
|
+
if (semiMajor <= 0 || semiMinor <= 0) {
|
|
318
|
+
throw new RangeError('semiMajor and semiMinor must be positive');
|
|
319
|
+
}
|
|
320
|
+
if (segments < 8) {
|
|
321
|
+
throw new RangeError('segments must be >= 8');
|
|
322
|
+
}
|
|
323
|
+
const cosR = Math.cos(rotation);
|
|
324
|
+
const sinR = Math.sin(rotation);
|
|
325
|
+
const ring = [];
|
|
326
|
+
for (let i = 0; i < segments; i++) {
|
|
327
|
+
const theta = (i / segments) * Math.PI * 2;
|
|
328
|
+
// Ellipse in local axis-aligned frame, then rotated by `rotation`.
|
|
329
|
+
const ax = Math.cos(theta) * semiMajor;
|
|
330
|
+
const ay = Math.sin(theta) * semiMinor;
|
|
331
|
+
const dx = ax * cosR - ay * sinR;
|
|
332
|
+
const dy = ax * sinR + ay * cosR;
|
|
333
|
+
ring.push(this.offsetMetersToLonLat(center, dx, dy));
|
|
334
|
+
}
|
|
335
|
+
ring.push(ring[0]); // close the ring
|
|
336
|
+
return {
|
|
337
|
+
id: this.nextId('ellipse'),
|
|
338
|
+
geometry: { type: 'Polygon', coordinates: [ring] },
|
|
339
|
+
properties,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Build a `Feature<Polygon>` for a circular sector (pie slice).
|
|
344
|
+
* See {@link SectorConfig} for parameter semantics.
|
|
345
|
+
*/
|
|
346
|
+
createSector(config) {
|
|
347
|
+
const { center, radius, startAngle, endAngle, segments = 32, properties } = config;
|
|
348
|
+
if (radius <= 0) {
|
|
349
|
+
throw new RangeError('radius must be positive');
|
|
350
|
+
}
|
|
351
|
+
if (endAngle <= startAngle) {
|
|
352
|
+
throw new RangeError('endAngle must be greater than startAngle');
|
|
353
|
+
}
|
|
354
|
+
if (endAngle - startAngle > Math.PI * 2) {
|
|
355
|
+
throw new RangeError('sector cannot exceed full circle');
|
|
356
|
+
}
|
|
357
|
+
if (segments < 4) {
|
|
358
|
+
throw new RangeError('segments must be >= 4');
|
|
359
|
+
}
|
|
360
|
+
const ring = [center];
|
|
361
|
+
const span = endAngle - startAngle;
|
|
362
|
+
for (let i = 0; i <= segments; i++) {
|
|
363
|
+
const theta = startAngle + (i / segments) * span;
|
|
364
|
+
const dx = Math.cos(theta) * radius;
|
|
365
|
+
const dy = Math.sin(theta) * radius;
|
|
366
|
+
ring.push(this.offsetMetersToLonLat(center, dx, dy));
|
|
367
|
+
}
|
|
368
|
+
ring.push(center); // close back to apex
|
|
369
|
+
return {
|
|
370
|
+
id: this.nextId('sector'),
|
|
371
|
+
geometry: { type: 'Polygon', coordinates: [ring] },
|
|
372
|
+
properties,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Build a `Feature<Polygon>` for a donut (annular ring). The output has
|
|
377
|
+
* two rings: an outer ring wound counter-clockwise and an inner ring
|
|
378
|
+
* wound clockwise so the GeoJSON right-hand rule renders the hole.
|
|
379
|
+
*/
|
|
380
|
+
createDonut(config) {
|
|
381
|
+
const { center, outerRadius, innerRadius, segments = 64, properties } = config;
|
|
382
|
+
if (outerRadius <= 0 || innerRadius <= 0) {
|
|
383
|
+
throw new RangeError('radii must be positive');
|
|
384
|
+
}
|
|
385
|
+
if (outerRadius <= innerRadius) {
|
|
386
|
+
throw new RangeError('outerRadius must be greater than innerRadius');
|
|
387
|
+
}
|
|
388
|
+
if (segments < 8) {
|
|
389
|
+
throw new RangeError('segments must be >= 8');
|
|
390
|
+
}
|
|
391
|
+
const outer = [];
|
|
392
|
+
const inner = [];
|
|
393
|
+
for (let i = 0; i < segments; i++) {
|
|
394
|
+
const theta = (i / segments) * Math.PI * 2;
|
|
395
|
+
const cosT = Math.cos(theta);
|
|
396
|
+
const sinT = Math.sin(theta);
|
|
397
|
+
// Outer ring: CCW (theta increasing)
|
|
398
|
+
outer.push(this.offsetMetersToLonLat(center, cosT * outerRadius, sinT * outerRadius));
|
|
399
|
+
// Inner ring: CW — sample the SAME thetas but we'll reverse the
|
|
400
|
+
// accumulator below so the ring is traversed in the opposite sense.
|
|
401
|
+
inner.push(this.offsetMetersToLonLat(center, cosT * innerRadius, sinT * innerRadius));
|
|
402
|
+
}
|
|
403
|
+
inner.reverse();
|
|
404
|
+
outer.push(outer[0]);
|
|
405
|
+
inner.push(inner[0]);
|
|
406
|
+
return {
|
|
407
|
+
id: this.nextId('donut'),
|
|
408
|
+
geometry: { type: 'Polygon', coordinates: [outer, inner] },
|
|
409
|
+
properties,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Project an `(dx, dy)` meter offset from `center` to lon/lat using true
|
|
414
|
+
* geodesic math (Vincenty's formulae) via ol/sphere.
|
|
415
|
+
*/
|
|
416
|
+
offsetMetersToLonLat(center, dx, dy) {
|
|
417
|
+
if (dx === 0 && dy === 0)
|
|
418
|
+
return [...center];
|
|
419
|
+
const distance = Math.hypot(dx, dy);
|
|
420
|
+
const bearing = Math.atan2(dx, dy);
|
|
421
|
+
return offset(center, distance, bearing);
|
|
422
|
+
}
|
|
423
|
+
nextId(kind) {
|
|
424
|
+
return `${kind}-${++this.idCounter}`;
|
|
425
|
+
}
|
|
426
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlGeometryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
427
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlGeometryService, providedIn: 'root' });
|
|
428
|
+
}
|
|
429
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlGeometryService, decorators: [{
|
|
430
|
+
type: Injectable,
|
|
431
|
+
args: [{
|
|
432
|
+
providedIn: 'root',
|
|
433
|
+
}]
|
|
434
|
+
}] });
|
|
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
|
+
|
|
285
646
|
// Provider functions
|
|
286
647
|
function provideOpenLayers(...features) {
|
|
287
648
|
return makeEnvironmentProviders([
|
|
@@ -291,10 +652,43 @@ function provideOpenLayers(...features) {
|
|
|
291
652
|
]);
|
|
292
653
|
}
|
|
293
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Registers custom projections using proj4.
|
|
657
|
+
*
|
|
658
|
+
* @param proj4 - The proj4 instance (must be passed to avoid strong dependency on proj4 package)
|
|
659
|
+
* @param definitions - Array of projection definitions
|
|
660
|
+
* @returns OlFeature for projections
|
|
661
|
+
*/
|
|
662
|
+
function withProjections(proj4, definitions) {
|
|
663
|
+
return {
|
|
664
|
+
kind: 'projections',
|
|
665
|
+
providers: [
|
|
666
|
+
{
|
|
667
|
+
provide: ENVIRONMENT_INITIALIZER,
|
|
668
|
+
multi: true,
|
|
669
|
+
useValue: () => {
|
|
670
|
+
definitions.forEach((d) => {
|
|
671
|
+
proj4.defs(d.code, d.def);
|
|
672
|
+
});
|
|
673
|
+
register(proj4);
|
|
674
|
+
definitions.forEach((d) => {
|
|
675
|
+
if (d.extent) {
|
|
676
|
+
const proj = get(d.code);
|
|
677
|
+
if (proj) {
|
|
678
|
+
proj.setExtent(d.extent);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
],
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
294
688
|
// @angular-helpers/openlayers/core
|
|
295
689
|
|
|
296
690
|
/**
|
|
297
691
|
* Generated bundle index. Do not edit.
|
|
298
692
|
*/
|
|
299
693
|
|
|
300
|
-
export { OlMapComponent, OlMapService, OlZoneHelper, provideOpenLayers };
|
|
694
|
+
export { OlGeometryService, OlMapComponent, OlMapService, OlTimeService, OlZoneHelper, createVectorResource, featureToOlFeature, olFeatureToFeature, provideOpenLayers, withProjections };
|