@angular-helpers/openlayers 0.5.1 → 0.7.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 +112 -1
- package/fesm2022/angular-helpers-openlayers-controls.mjs +233 -3
- package/fesm2022/angular-helpers-openlayers-core.mjs +49 -26
- package/fesm2022/angular-helpers-openlayers-layers.mjs +45 -5
- package/package.json +1 -1
- package/types/angular-helpers-openlayers-controls.d.ts +52 -2
- package/types/angular-helpers-openlayers-core.d.ts +12 -4
- package/types/angular-helpers-openlayers-layers.d.ts +3 -1
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`.
|
|
@@ -227,6 +276,68 @@ Renders points, lines, and polygons using WebGL 2. For peak performance, hit det
|
|
|
227
276
|
|
|
228
277
|
Rigorous cleanup guarantees that WebGL contexts, framebuffers, and active buffers are fully released on destroy (`layer.dispose()`), preventing GPU leaks.
|
|
229
278
|
|
|
279
|
+
## Time-Series Animation & Playback
|
|
280
|
+
|
|
281
|
+
Available since `0.5.0` from `@angular-helpers/openlayers/core` and `@angular-helpers/openlayers/controls`.
|
|
282
|
+
|
|
283
|
+
Provides high-performance, GPU-friendly reactive animation controls for time-series geospatial visualization (e.g. weather radar, vehicle tracking, historical paths).
|
|
284
|
+
|
|
285
|
+
### 1. `OlTimeService` — 60FPS Animation Loop
|
|
286
|
+
|
|
287
|
+
The core service coordinates playback timing. It runs its timing ticks entirely outside the Angular zone (using `requestAnimationFrame`) to prevent triggering global Angular change detection cycles at 60FPS, while exposing reactive signals for component consumption.
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import { inject, Component } from '@angular/core';
|
|
291
|
+
import { OlTimeService } from '@angular-helpers/openlayers/core';
|
|
292
|
+
|
|
293
|
+
@Component({
|
|
294
|
+
template: `<p>Current Time: {{ timeSvc.currentTime() }}</p>`,
|
|
295
|
+
})
|
|
296
|
+
export class MapAnimation {
|
|
297
|
+
protected timeSvc = inject(OlTimeService);
|
|
298
|
+
|
|
299
|
+
constructor() {
|
|
300
|
+
// Start animation loop
|
|
301
|
+
this.timeSvc.play();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 2. `<ol-timeline>` — Premium Playback UI Control
|
|
307
|
+
|
|
308
|
+
A sleek, glassmorphic UI overlay component displaying a timeline scrubber, play/pause toggle, and playback speed multiplier.
|
|
309
|
+
|
|
310
|
+
```html
|
|
311
|
+
<ol-map [center]="[2.17, 41.38]" [zoom]="12">
|
|
312
|
+
<ol-tile-layer source="osm" />
|
|
313
|
+
|
|
314
|
+
<ol-timeline
|
|
315
|
+
[startTime]="1700000000000"
|
|
316
|
+
[endTime]="1700086400000"
|
|
317
|
+
[playSpeed]="60"
|
|
318
|
+
[loop]="true"
|
|
319
|
+
position="bottom-center"
|
|
320
|
+
(timeChange)="onTimeChange($event)"
|
|
321
|
+
/>
|
|
322
|
+
</ol-map>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
| Input | Type | Default | Description |
|
|
326
|
+
| ------------- | -------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------- |
|
|
327
|
+
| `startTime` | `number` | _Required_ | Start bounds of the time-series (Epoch ms) |
|
|
328
|
+
| `endTime` | `number` | _Required_ | End bounds of the time-series (Epoch ms) |
|
|
329
|
+
| `playSpeed` | `number` | `1` | Default speed multiplier (e.g. 1, 5, 10, 60, 3600) |
|
|
330
|
+
| `loop` | `boolean` | `false` | Loop playback when reaching `endTime` |
|
|
331
|
+
| `position` | `TimelinePosition` | `'bottom-center'` | Control alignment (`top-left`, `top-center`, `top-right`, `bottom-left`, `bottom-center`, `bottom-right`) |
|
|
332
|
+
| `formatLabel` | `(time: number) => string` | `new Date(t).toLocaleString()` | Custom label formatter for the time display |
|
|
333
|
+
|
|
334
|
+
| Output | Type | Description |
|
|
335
|
+
| ----------------- | --------- | --------------------------------------------------------------------------------- |
|
|
336
|
+
| `timeChange` | `number` | Emitted with the current active epoch timestamp when the timeline ticks or scrubs |
|
|
337
|
+
| `playStateChange` | `boolean` | Emitted when playback transitions between playing (`true`) and paused (`false`) |
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
230
341
|
## Geodesic Geometry Helpers
|
|
231
342
|
|
|
232
343
|
Available from `@angular-helpers/openlayers/core` via `OlGeometryService`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, input, DestroyRef, afterNextRender, ChangeDetectionStrategy, Component, InjectionToken, output, signal, ViewChild, Injectable } from '@angular/core';
|
|
3
|
-
import { OlMapService, OlZoneHelper } from '@angular-helpers/openlayers/core';
|
|
2
|
+
import { inject, input, DestroyRef, afterNextRender, ChangeDetectionStrategy, Component, InjectionToken, output, signal, ViewChild, Injectable, computed, effect } from '@angular/core';
|
|
3
|
+
import { OlMapService, OlZoneHelper, OlTimeService } from '@angular-helpers/openlayers/core';
|
|
4
4
|
import Zoom from 'ol/control/Zoom';
|
|
5
5
|
import Attribution from 'ol/control/Attribution';
|
|
6
6
|
import ScaleLine from 'ol/control/ScaleLine';
|
|
@@ -857,10 +857,240 @@ function provideControls() {
|
|
|
857
857
|
return withControls();
|
|
858
858
|
}
|
|
859
859
|
|
|
860
|
+
/**
|
|
861
|
+
* A premium, reactive visual timeline component for OpenLayers.
|
|
862
|
+
* Orchestrates time-series animations by binding directly to OlTimeService.
|
|
863
|
+
*
|
|
864
|
+
* Uses native controls, CSS grid/flex layouts, and a sleek glassmorphic theme.
|
|
865
|
+
* Completely free of CommonModule or FormsModule dependencies for maximum performance.
|
|
866
|
+
*
|
|
867
|
+
* @usageNotes
|
|
868
|
+
* ```html
|
|
869
|
+
* <ol-timeline
|
|
870
|
+
* [startTime]="1700000000000"
|
|
871
|
+
* [endTime]="1700086400000"
|
|
872
|
+
* [playSpeed]="60"
|
|
873
|
+
* [loop]="true"
|
|
874
|
+
* position="bottom-center">
|
|
875
|
+
* </ol-timeline>
|
|
876
|
+
* ```
|
|
877
|
+
*/
|
|
878
|
+
class OlTimelineComponent {
|
|
879
|
+
timeService = inject(OlTimeService);
|
|
880
|
+
/** Start bounds of time-series (Epoch ms) */
|
|
881
|
+
startTime = input.required(...(ngDevMode ? [{ debugName: "startTime" }] : /* istanbul ignore next */ []));
|
|
882
|
+
/** End bounds of time-series (Epoch ms) */
|
|
883
|
+
endTime = input.required(...(ngDevMode ? [{ debugName: "endTime" }] : /* istanbul ignore next */ []));
|
|
884
|
+
/** Default speed multiplier (e.g. 1, 5, 10, 60, 3600) */
|
|
885
|
+
playSpeed = input(1, ...(ngDevMode ? [{ debugName: "playSpeed" }] : /* istanbul ignore next */ []));
|
|
886
|
+
/** Loop playback when reaching endTime */
|
|
887
|
+
loop = input(false, ...(ngDevMode ? [{ debugName: "loop" }] : /* istanbul ignore next */ []));
|
|
888
|
+
/** Position overlay alignment */
|
|
889
|
+
position = input('bottom-center', ...(ngDevMode ? [{ debugName: "position" }] : /* istanbul ignore next */ []));
|
|
890
|
+
/** Custom label formatter */
|
|
891
|
+
formatLabel = input((t) => new Date(t).toLocaleString(), ...(ngDevMode ? [{ debugName: "formatLabel" }] : /* istanbul ignore next */ []));
|
|
892
|
+
/** Outputs */
|
|
893
|
+
timeChange = output();
|
|
894
|
+
playStateChange = output();
|
|
895
|
+
/** Computeds binding directly to OlTimeService */
|
|
896
|
+
currentTime = computed(() => this.timeService.currentTime(), ...(ngDevMode ? [{ debugName: "currentTime" }] : /* istanbul ignore next */ []));
|
|
897
|
+
isPlaying = computed(() => this.timeService.isPlaying(), ...(ngDevMode ? [{ debugName: "isPlaying" }] : /* istanbul ignore next */ []));
|
|
898
|
+
speed = computed(() => this.timeService.speed(), ...(ngDevMode ? [{ debugName: "speed" }] : /* istanbul ignore next */ []));
|
|
899
|
+
formattedTime = computed(() => this.formatLabel()(this.currentTime()), ...(ngDevMode ? [{ debugName: "formattedTime" }] : /* istanbul ignore next */ []));
|
|
900
|
+
constructor() {
|
|
901
|
+
// Sync default configuration settings when the inputs initialize
|
|
902
|
+
effect(() => {
|
|
903
|
+
this.timeService.setSpeed(this.playSpeed());
|
|
904
|
+
});
|
|
905
|
+
// Make sure initial time starts at the startTime bounds
|
|
906
|
+
effect(() => {
|
|
907
|
+
this.timeService.setTime(this.startTime());
|
|
908
|
+
});
|
|
909
|
+
// Reactive time-based loop boundaries check
|
|
910
|
+
effect(() => {
|
|
911
|
+
const current = this.currentTime();
|
|
912
|
+
const end = this.endTime();
|
|
913
|
+
const isAnimPlaying = this.isPlaying();
|
|
914
|
+
if (current >= end && isAnimPlaying) {
|
|
915
|
+
if (this.loop()) {
|
|
916
|
+
this.timeService.setTime(this.startTime());
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
this.timeService.pause();
|
|
920
|
+
this.timeService.setTime(end);
|
|
921
|
+
this.playStateChange.emit(false);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
// Emit reactive output when current time advances
|
|
926
|
+
effect(() => {
|
|
927
|
+
this.timeChange.emit(this.currentTime());
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
togglePlay() {
|
|
931
|
+
if (this.isPlaying()) {
|
|
932
|
+
this.timeService.pause();
|
|
933
|
+
this.playStateChange.emit(false);
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
// If we are at the end, reset to start before playing
|
|
937
|
+
if (this.currentTime() >= this.endTime()) {
|
|
938
|
+
this.timeService.setTime(this.startTime());
|
|
939
|
+
}
|
|
940
|
+
this.timeService.play();
|
|
941
|
+
this.playStateChange.emit(true);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
onScrub(event) {
|
|
945
|
+
const target = event.target;
|
|
946
|
+
const value = Number(target.value);
|
|
947
|
+
this.timeService.setTime(value);
|
|
948
|
+
}
|
|
949
|
+
onSpeedChange(event) {
|
|
950
|
+
const target = event.target;
|
|
951
|
+
const value = Number(target.value);
|
|
952
|
+
this.timeService.setSpeed(value);
|
|
953
|
+
}
|
|
954
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimelineComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
955
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: OlTimelineComponent, isStandalone: true, selector: "ol-timeline", inputs: { startTime: { classPropertyName: "startTime", publicName: "startTime", isSignal: true, isRequired: true, transformFunction: null }, endTime: { classPropertyName: "endTime", publicName: "endTime", isSignal: true, isRequired: true, transformFunction: null }, playSpeed: { classPropertyName: "playSpeed", publicName: "playSpeed", isSignal: true, isRequired: false, transformFunction: null }, loop: { classPropertyName: "loop", publicName: "loop", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, formatLabel: { classPropertyName: "formatLabel", publicName: "formatLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { timeChange: "timeChange", playStateChange: "playStateChange" }, ngImport: i0, template: `
|
|
956
|
+
<div
|
|
957
|
+
class="ol-timeline"
|
|
958
|
+
[class.ol-timeline--top-left]="position() === 'top-left'"
|
|
959
|
+
[class.ol-timeline--top-center]="position() === 'top-center'"
|
|
960
|
+
[class.ol-timeline--top-right]="position() === 'top-right'"
|
|
961
|
+
[class.ol-timeline--bottom-left]="position() === 'bottom-left'"
|
|
962
|
+
[class.ol-timeline--bottom-center]="position() === 'bottom-center'"
|
|
963
|
+
[class.ol-timeline--bottom-right]="position() === 'bottom-right'"
|
|
964
|
+
>
|
|
965
|
+
<div class="ol-timeline__controls">
|
|
966
|
+
<button
|
|
967
|
+
type="button"
|
|
968
|
+
class="ol-timeline__btn ol-timeline__btn--play"
|
|
969
|
+
(click)="togglePlay()"
|
|
970
|
+
[attr.aria-label]="isPlaying() ? 'Pause animation' : 'Play animation'"
|
|
971
|
+
>
|
|
972
|
+
@if (isPlaying()) {
|
|
973
|
+
<!-- Pause Icon -->
|
|
974
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
|
975
|
+
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
|
976
|
+
</svg>
|
|
977
|
+
} @else {
|
|
978
|
+
<!-- Play Icon -->
|
|
979
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
|
980
|
+
<path d="M8 5v14l11-7z" />
|
|
981
|
+
</svg>
|
|
982
|
+
}
|
|
983
|
+
</button>
|
|
984
|
+
|
|
985
|
+
<span class="ol-timeline__time-display" aria-live="polite">
|
|
986
|
+
{{ formattedTime() }}
|
|
987
|
+
</span>
|
|
988
|
+
</div>
|
|
989
|
+
|
|
990
|
+
<div class="ol-timeline__slider-container">
|
|
991
|
+
<input
|
|
992
|
+
type="range"
|
|
993
|
+
class="ol-timeline__slider"
|
|
994
|
+
[min]="startTime()"
|
|
995
|
+
[max]="endTime()"
|
|
996
|
+
[value]="currentTime()"
|
|
997
|
+
(input)="onScrub($event)"
|
|
998
|
+
aria-label="Timeline progress slider"
|
|
999
|
+
/>
|
|
1000
|
+
</div>
|
|
1001
|
+
|
|
1002
|
+
<div class="ol-timeline__settings">
|
|
1003
|
+
<select
|
|
1004
|
+
class="ol-timeline__speed-select"
|
|
1005
|
+
[value]="speed()"
|
|
1006
|
+
(change)="onSpeedChange($event)"
|
|
1007
|
+
aria-label="Playback speed multiplier"
|
|
1008
|
+
>
|
|
1009
|
+
<option [value]="1">1x (Real)</option>
|
|
1010
|
+
<option [value]="5">5x</option>
|
|
1011
|
+
<option [value]="10">10x</option>
|
|
1012
|
+
<option [value]="30">30x</option>
|
|
1013
|
+
<option [value]="60">60x (1m/s)</option>
|
|
1014
|
+
<option [value]="300">300x (5m/s)</option>
|
|
1015
|
+
<option [value]="3600">3600x (1h/s)</option>
|
|
1016
|
+
</select>
|
|
1017
|
+
</div>
|
|
1018
|
+
</div>
|
|
1019
|
+
`, isInline: true, styles: [":host{display:block}.ol-timeline{position:absolute;display:flex;align-items:center;gap:16px;padding:8px 16px;background:#1e1e1ebf;backdrop-filter:blur(12px) saturate(160%);-webkit-backdrop-filter:blur(12px) saturate(160%);border:1px solid rgba(255,255,255,.08);border-radius:24px;box-shadow:0 8px 32px #0000004d;color:#f3f3f3;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:13px;z-index:10;width:calc(100% - 32px);max-width:600px;box-sizing:border-box}.ol-timeline--top-left{top:12px;left:12px}.ol-timeline--top-center{top:12px;left:50%;transform:translate(-50%)}.ol-timeline--top-right{top:12px;right:12px}.ol-timeline--bottom-left{bottom:12px;left:12px}.ol-timeline--bottom-center{bottom:24px;left:50%;transform:translate(-50%)}.ol-timeline--bottom-right{bottom:12px;right:12px}.ol-timeline__controls{display:flex;align-items:center;gap:12px;flex-shrink:0}.ol-timeline__btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border:none;border-radius:50%;background:#ffffff1a;color:#fff;cursor:pointer;transition:background .15s ease,transform .15s ease}.ol-timeline__btn:hover{background:#fff3;transform:scale(1.05)}.ol-timeline__btn:active{transform:scale(.95)}.ol-timeline__time-display{font-variant-numeric:tabular-nums;font-weight:500;color:#e0e0e0;min-width:140px;text-align:center}.ol-timeline__slider-container{flex-grow:1;display:flex;align-items:center}.ol-timeline__slider{-webkit-appearance:none;width:100%;height:4px;border-radius:2px;background:#fff3;outline:none;cursor:pointer;transition:background .15s ease}.ol-timeline__slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#1a73e8;box-shadow:0 1px 4px #0006;cursor:pointer;transition:background .15s ease,transform .15s ease}.ol-timeline__slider::-webkit-slider-thumb:hover{background:#2b84f0;transform:scale(1.15)}.ol-timeline__slider::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:#1a73e8;box-shadow:0 1px 4px #0006;cursor:pointer;transition:background .15s ease,transform .15s ease}.ol-timeline__slider::-moz-range-thumb:hover{background:#2b84f0;transform:scale(1.15)}.ol-timeline__settings{flex-shrink:0}.ol-timeline__speed-select{background:#ffffff1a;color:#fff;border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:4px 8px;font-size:11px;font-weight:500;outline:none;cursor:pointer;transition:background .15s ease}.ol-timeline__speed-select:hover{background:#ffffff26}.ol-timeline__speed-select option{background:#1e1e1e;color:#fff}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1020
|
+
}
|
|
1021
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlTimelineComponent, decorators: [{
|
|
1022
|
+
type: Component,
|
|
1023
|
+
args: [{ selector: 'ol-timeline', standalone: true, imports: [], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
1024
|
+
<div
|
|
1025
|
+
class="ol-timeline"
|
|
1026
|
+
[class.ol-timeline--top-left]="position() === 'top-left'"
|
|
1027
|
+
[class.ol-timeline--top-center]="position() === 'top-center'"
|
|
1028
|
+
[class.ol-timeline--top-right]="position() === 'top-right'"
|
|
1029
|
+
[class.ol-timeline--bottom-left]="position() === 'bottom-left'"
|
|
1030
|
+
[class.ol-timeline--bottom-center]="position() === 'bottom-center'"
|
|
1031
|
+
[class.ol-timeline--bottom-right]="position() === 'bottom-right'"
|
|
1032
|
+
>
|
|
1033
|
+
<div class="ol-timeline__controls">
|
|
1034
|
+
<button
|
|
1035
|
+
type="button"
|
|
1036
|
+
class="ol-timeline__btn ol-timeline__btn--play"
|
|
1037
|
+
(click)="togglePlay()"
|
|
1038
|
+
[attr.aria-label]="isPlaying() ? 'Pause animation' : 'Play animation'"
|
|
1039
|
+
>
|
|
1040
|
+
@if (isPlaying()) {
|
|
1041
|
+
<!-- Pause Icon -->
|
|
1042
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
|
1043
|
+
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" />
|
|
1044
|
+
</svg>
|
|
1045
|
+
} @else {
|
|
1046
|
+
<!-- Play Icon -->
|
|
1047
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
|
1048
|
+
<path d="M8 5v14l11-7z" />
|
|
1049
|
+
</svg>
|
|
1050
|
+
}
|
|
1051
|
+
</button>
|
|
1052
|
+
|
|
1053
|
+
<span class="ol-timeline__time-display" aria-live="polite">
|
|
1054
|
+
{{ formattedTime() }}
|
|
1055
|
+
</span>
|
|
1056
|
+
</div>
|
|
1057
|
+
|
|
1058
|
+
<div class="ol-timeline__slider-container">
|
|
1059
|
+
<input
|
|
1060
|
+
type="range"
|
|
1061
|
+
class="ol-timeline__slider"
|
|
1062
|
+
[min]="startTime()"
|
|
1063
|
+
[max]="endTime()"
|
|
1064
|
+
[value]="currentTime()"
|
|
1065
|
+
(input)="onScrub($event)"
|
|
1066
|
+
aria-label="Timeline progress slider"
|
|
1067
|
+
/>
|
|
1068
|
+
</div>
|
|
1069
|
+
|
|
1070
|
+
<div class="ol-timeline__settings">
|
|
1071
|
+
<select
|
|
1072
|
+
class="ol-timeline__speed-select"
|
|
1073
|
+
[value]="speed()"
|
|
1074
|
+
(change)="onSpeedChange($event)"
|
|
1075
|
+
aria-label="Playback speed multiplier"
|
|
1076
|
+
>
|
|
1077
|
+
<option [value]="1">1x (Real)</option>
|
|
1078
|
+
<option [value]="5">5x</option>
|
|
1079
|
+
<option [value]="10">10x</option>
|
|
1080
|
+
<option [value]="30">30x</option>
|
|
1081
|
+
<option [value]="60">60x (1m/s)</option>
|
|
1082
|
+
<option [value]="300">300x (5m/s)</option>
|
|
1083
|
+
<option [value]="3600">3600x (1h/s)</option>
|
|
1084
|
+
</select>
|
|
1085
|
+
</div>
|
|
1086
|
+
</div>
|
|
1087
|
+
`, styles: [":host{display:block}.ol-timeline{position:absolute;display:flex;align-items:center;gap:16px;padding:8px 16px;background:#1e1e1ebf;backdrop-filter:blur(12px) saturate(160%);-webkit-backdrop-filter:blur(12px) saturate(160%);border:1px solid rgba(255,255,255,.08);border-radius:24px;box-shadow:0 8px 32px #0000004d;color:#f3f3f3;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:13px;z-index:10;width:calc(100% - 32px);max-width:600px;box-sizing:border-box}.ol-timeline--top-left{top:12px;left:12px}.ol-timeline--top-center{top:12px;left:50%;transform:translate(-50%)}.ol-timeline--top-right{top:12px;right:12px}.ol-timeline--bottom-left{bottom:12px;left:12px}.ol-timeline--bottom-center{bottom:24px;left:50%;transform:translate(-50%)}.ol-timeline--bottom-right{bottom:12px;right:12px}.ol-timeline__controls{display:flex;align-items:center;gap:12px;flex-shrink:0}.ol-timeline__btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border:none;border-radius:50%;background:#ffffff1a;color:#fff;cursor:pointer;transition:background .15s ease,transform .15s ease}.ol-timeline__btn:hover{background:#fff3;transform:scale(1.05)}.ol-timeline__btn:active{transform:scale(.95)}.ol-timeline__time-display{font-variant-numeric:tabular-nums;font-weight:500;color:#e0e0e0;min-width:140px;text-align:center}.ol-timeline__slider-container{flex-grow:1;display:flex;align-items:center}.ol-timeline__slider{-webkit-appearance:none;width:100%;height:4px;border-radius:2px;background:#fff3;outline:none;cursor:pointer;transition:background .15s ease}.ol-timeline__slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#1a73e8;box-shadow:0 1px 4px #0006;cursor:pointer;transition:background .15s ease,transform .15s ease}.ol-timeline__slider::-webkit-slider-thumb:hover{background:#2b84f0;transform:scale(1.15)}.ol-timeline__slider::-moz-range-thumb{width:14px;height:14px;border:none;border-radius:50%;background:#1a73e8;box-shadow:0 1px 4px #0006;cursor:pointer;transition:background .15s ease,transform .15s ease}.ol-timeline__slider::-moz-range-thumb:hover{background:#2b84f0;transform:scale(1.15)}.ol-timeline__settings{flex-shrink:0}.ol-timeline__speed-select{background:#ffffff1a;color:#fff;border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:4px 8px;font-size:11px;font-weight:500;outline:none;cursor:pointer;transition:background .15s ease}.ol-timeline__speed-select:hover{background:#ffffff26}.ol-timeline__speed-select option{background:#1e1e1e;color:#fff}\n"] }]
|
|
1088
|
+
}], ctorParameters: () => [], propDecorators: { startTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "startTime", required: true }] }], endTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "endTime", required: true }] }], playSpeed: [{ type: i0.Input, args: [{ isSignal: true, alias: "playSpeed", required: false }] }], loop: [{ type: i0.Input, args: [{ isSignal: true, alias: "loop", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], formatLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "formatLabel", required: false }] }], timeChange: [{ type: i0.Output, args: ["timeChange"] }], playStateChange: [{ type: i0.Output, args: ["playStateChange"] }] } });
|
|
1089
|
+
|
|
860
1090
|
// @angular-helpers/openlayers/controls
|
|
861
1091
|
|
|
862
1092
|
/**
|
|
863
1093
|
* Generated bundle index. Do not edit.
|
|
864
1094
|
*/
|
|
865
1095
|
|
|
866
|
-
export { OlAttributionControlComponent, OlBasemapSwitcherComponent, OlControlService, OlFullscreenControlComponent, OlGeolocationControlComponent, OlLayerSwitcherComponent, OlRotateControlComponent, OlScaleLineControlComponent, OlZoomControlComponent, ROTATE_CONTROL_MAP_SERVICE, provideControls, withControls };
|
|
1096
|
+
export { OlAttributionControlComponent, OlBasemapSwitcherComponent, OlControlService, OlFullscreenControlComponent, OlGeolocationControlComponent, OlLayerSwitcherComponent, OlRotateControlComponent, OlScaleLineControlComponent, OlTimelineComponent, OlZoomControlComponent, ROTATE_CONTROL_MAP_SERVICE, provideControls, withControls };
|
|
@@ -2,7 +2,7 @@ import * as i0 from '@angular/core';
|
|
|
2
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
7
|
import GeoJSON from 'ol/format/GeoJSON';
|
|
8
8
|
import FeatureClass from 'ol/Feature';
|
|
@@ -164,6 +164,7 @@ class OlMapComponent {
|
|
|
164
164
|
zoom = input(0, ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
|
|
165
165
|
rotation = input(0, ...(ngDevMode ? [{ debugName: "rotation" }] : /* istanbul ignore next */ []));
|
|
166
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
|
|
167
168
|
viewChange = output();
|
|
168
169
|
mapClick = output();
|
|
169
170
|
mapDblClick = output();
|
|
@@ -190,11 +191,25 @@ class OlMapComponent {
|
|
|
190
191
|
// Cleanup when component is destroyed
|
|
191
192
|
this.destroyRef.onDestroy(() => this.destroyMap());
|
|
192
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
|
+
}
|
|
193
208
|
initMap() {
|
|
194
209
|
const container = this.mapContainerRef().nativeElement;
|
|
195
210
|
this.zoneHelper.runOutsideAngular(() => {
|
|
196
211
|
const view = new View({
|
|
197
|
-
center:
|
|
212
|
+
center: this.getProjectedCoordinate(this.center()),
|
|
198
213
|
zoom: this.zoom(),
|
|
199
214
|
rotation: this.rotation(),
|
|
200
215
|
projection: this.projection(),
|
|
@@ -222,11 +237,11 @@ class OlMapComponent {
|
|
|
222
237
|
});
|
|
223
238
|
});
|
|
224
239
|
this.map.on('click', (e) => this.zoneHelper.runInsideAngular(() => this.mapClick.emit({
|
|
225
|
-
coordinate:
|
|
240
|
+
coordinate: this.getExternalCoordinate(e.coordinate),
|
|
226
241
|
pixel: e.pixel,
|
|
227
242
|
})));
|
|
228
243
|
this.map.on('dblclick', (e) => this.zoneHelper.runInsideAngular(() => this.mapDblClick.emit({
|
|
229
|
-
coordinate:
|
|
244
|
+
coordinate: this.getExternalCoordinate(e.coordinate),
|
|
230
245
|
pixel: e.pixel,
|
|
231
246
|
})));
|
|
232
247
|
});
|
|
@@ -250,7 +265,7 @@ class OlMapComponent {
|
|
|
250
265
|
if (!this.map)
|
|
251
266
|
return;
|
|
252
267
|
const view = this.map.getView();
|
|
253
|
-
const projectedCenter =
|
|
268
|
+
const projectedCenter = this.getProjectedCoordinate(center);
|
|
254
269
|
const currentCenter = view.getCenter();
|
|
255
270
|
// Only update if center is significantly different (prevents interfering with animations)
|
|
256
271
|
if (!currentCenter ||
|
|
@@ -283,23 +298,23 @@ class OlMapComponent {
|
|
|
283
298
|
const view = this.map?.getView();
|
|
284
299
|
if (view) {
|
|
285
300
|
const projectedCenter = view.getCenter() ?? [0, 0];
|
|
286
|
-
const
|
|
301
|
+
const externalCenter = this.getExternalCoordinate(projectedCenter);
|
|
287
302
|
this.viewChange.emit({
|
|
288
|
-
center:
|
|
303
|
+
center: externalCenter,
|
|
289
304
|
zoom: view.getZoom() ?? 0,
|
|
290
305
|
rotation: view.getRotation() ?? 0,
|
|
291
306
|
});
|
|
292
307
|
}
|
|
293
308
|
}
|
|
294
309
|
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>
|
|
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>
|
|
296
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 });
|
|
297
312
|
}
|
|
298
313
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlMapComponent, decorators: [{
|
|
299
314
|
type: Component,
|
|
300
315
|
args: [{ selector: 'ol-map', template: `<div class="ol-map-container" #mapContainer></div>
|
|
301
316
|
<ng-content />`, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block;width:100%;height:100%;position:relative}.ol-map-container{width:100%;height:100%}\n"] }]
|
|
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 }] }] } });
|
|
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 }] }] } });
|
|
303
318
|
|
|
304
319
|
// OlGeometryService — general purpose geometry helpers
|
|
305
320
|
/**
|
|
@@ -511,44 +526,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
|
|
|
511
526
|
}] });
|
|
512
527
|
|
|
513
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
|
+
}
|
|
514
539
|
/**
|
|
515
540
|
* Converts an OpenLayers feature to the internal Feature format.
|
|
516
|
-
* Handles coordinate extraction and geometry type mapping.
|
|
541
|
+
* Handles coordinate extraction and geometry type mapping with custom projections.
|
|
517
542
|
*
|
|
518
543
|
* @param olFeature - The OpenLayers feature to convert
|
|
544
|
+
* @param options - Projection source and target codes
|
|
519
545
|
* @returns The converted Feature with normalized structure
|
|
520
546
|
*/
|
|
521
|
-
function olFeatureToFeature(olFeature) {
|
|
547
|
+
function olFeatureToFeature(olFeature, options) {
|
|
522
548
|
// Unwrap spider features
|
|
523
549
|
const spiderFeature = olFeature.get('spider-feature');
|
|
524
550
|
if (spiderFeature) {
|
|
525
|
-
return olFeatureToFeature(spiderFeature);
|
|
551
|
+
return olFeatureToFeature(spiderFeature, options);
|
|
526
552
|
}
|
|
527
553
|
// Unwrap single-item clusters
|
|
528
554
|
const clusterFeatures = olFeature.get('features');
|
|
529
555
|
if (Array.isArray(clusterFeatures) && clusterFeatures.length === 1) {
|
|
530
|
-
return olFeatureToFeature(clusterFeatures[0]);
|
|
556
|
+
return olFeatureToFeature(clusterFeatures[0], options);
|
|
531
557
|
}
|
|
532
558
|
const geometry = olFeature.getGeometry();
|
|
533
559
|
const geomType = geometry?.getType() ?? 'Point';
|
|
560
|
+
const sourceProj = options?.targetProjection;
|
|
561
|
+
const targetProj = options?.sourceProjection;
|
|
534
562
|
// Convert coordinates based on geometry type
|
|
535
563
|
let coordinates;
|
|
536
564
|
if (geomType === 'Circle') {
|
|
537
565
|
// ol/geom/Circle has no getCoordinates() — use getCenter() instead
|
|
538
566
|
const circle = geometry;
|
|
539
|
-
coordinates = circle.getCenter();
|
|
567
|
+
coordinates = transformCoords(circle.getCenter(), sourceProj, targetProj);
|
|
540
568
|
}
|
|
541
569
|
else {
|
|
542
570
|
// oxlint-disable-next-line no-explicit-any
|
|
543
571
|
const olCoords = geometry.getCoordinates();
|
|
544
|
-
|
|
545
|
-
// Multi-coordinate structures (LineString, Polygon, etc.)
|
|
546
|
-
coordinates = olCoords;
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
// Single point
|
|
550
|
-
coordinates = olCoords;
|
|
551
|
-
}
|
|
572
|
+
coordinates = transformCoords(olCoords, sourceProj, targetProj);
|
|
552
573
|
}
|
|
553
574
|
return {
|
|
554
575
|
id: olFeature.getId()?.toString() ?? `feature-${Math.random().toString(36).slice(2)}`,
|
|
@@ -562,7 +583,9 @@ function olFeatureToFeature(olFeature) {
|
|
|
562
583
|
/**
|
|
563
584
|
* Converts an internal Feature to an OpenLayers feature.
|
|
564
585
|
*/
|
|
565
|
-
function featureToOlFeature(feature) {
|
|
586
|
+
function featureToOlFeature(feature, options) {
|
|
587
|
+
const sourceProj = options?.sourceProjection ?? 'EPSG:4326';
|
|
588
|
+
const targetProj = options?.targetProjection ?? 'EPSG:3857';
|
|
566
589
|
const geom = feature.geometry;
|
|
567
590
|
let geometry;
|
|
568
591
|
if (!geom.coordinates) {
|
|
@@ -570,14 +593,14 @@ function featureToOlFeature(feature) {
|
|
|
570
593
|
}
|
|
571
594
|
else if (geom.type === 'Point') {
|
|
572
595
|
const coords = geom.coordinates;
|
|
573
|
-
geometry = new Point(
|
|
596
|
+
geometry = new Point(transformCoords(coords, sourceProj, targetProj));
|
|
574
597
|
}
|
|
575
598
|
else if (geom.type === 'LineString') {
|
|
576
|
-
const coords = geom.coordinates
|
|
599
|
+
const coords = transformCoords(geom.coordinates, sourceProj, targetProj);
|
|
577
600
|
geometry = new LineString(coords);
|
|
578
601
|
}
|
|
579
602
|
else if (geom.type === 'Polygon') {
|
|
580
|
-
const rings = geom.coordinates
|
|
603
|
+
const rings = transformCoords(geom.coordinates, sourceProj, targetProj);
|
|
581
604
|
geometry = new Polygon(rings);
|
|
582
605
|
}
|
|
583
606
|
else {
|
|
@@ -166,12 +166,13 @@ function buildVectorLayer(config, source) {
|
|
|
166
166
|
layer.set('id', config.id);
|
|
167
167
|
layer.set('cluster-config', clusterCfg);
|
|
168
168
|
layer.set('style-fn', styleFn);
|
|
169
|
+
layer.set('coordinate-projection', config.coordinateProjection);
|
|
169
170
|
return layer;
|
|
170
171
|
}
|
|
171
172
|
function buildHeatmapLayer(config) {
|
|
172
173
|
const vectorSource = new VectorSource();
|
|
173
174
|
if (config.features && config.features.length > 0) {
|
|
174
|
-
const olFeatures = config.features.map(featureToOlFeature);
|
|
175
|
+
const olFeatures = config.features.map((f) => featureToOlFeature(f));
|
|
175
176
|
vectorSource.addFeatures(olFeatures);
|
|
176
177
|
}
|
|
177
178
|
const layer = new HeatmapLayer({
|
|
@@ -392,9 +393,16 @@ class OlLayerService {
|
|
|
392
393
|
sourceOptions.format = new KML();
|
|
393
394
|
}
|
|
394
395
|
const vectorSource = new VectorSource(sourceOptions);
|
|
396
|
+
const targetProj = (typeof map.getView === 'function'
|
|
397
|
+
? map.getView()?.getProjection()?.getCode()
|
|
398
|
+
: undefined) ?? 'EPSG:3857';
|
|
399
|
+
const sourceProj = vConfig.coordinateProjection ?? 'EPSG:4326';
|
|
395
400
|
if (vConfig.features && vConfig.features.length > 0) {
|
|
396
401
|
const olFeatures = vConfig.features.map((f) => {
|
|
397
|
-
const olf = featureToOlFeature(f
|
|
402
|
+
const olf = featureToOlFeature(f, {
|
|
403
|
+
sourceProjection: sourceProj,
|
|
404
|
+
targetProjection: targetProj,
|
|
405
|
+
});
|
|
398
406
|
if (f.style)
|
|
399
407
|
olf.set(STYLE_PROP, f.style);
|
|
400
408
|
return olf;
|
|
@@ -462,6 +470,28 @@ class OlLayerService {
|
|
|
462
470
|
const layer = this.layerCache.get(id);
|
|
463
471
|
if (map && layer) {
|
|
464
472
|
map.removeLayer(layer);
|
|
473
|
+
// Explicitly dispose sources to prevent memory leaks
|
|
474
|
+
if ('getSource' in layer) {
|
|
475
|
+
const source = layer.getSource();
|
|
476
|
+
if (source) {
|
|
477
|
+
// If it's a ClusterSource, dispose the underlying source first
|
|
478
|
+
if ('getSource' in source && typeof source.getSource === 'function') {
|
|
479
|
+
const underlyingSource = source.getSource();
|
|
480
|
+
if (underlyingSource && typeof underlyingSource.dispose === 'function') {
|
|
481
|
+
if (typeof underlyingSource.clear === 'function') {
|
|
482
|
+
underlyingSource.clear(true);
|
|
483
|
+
}
|
|
484
|
+
underlyingSource.dispose();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (typeof source.dispose === 'function') {
|
|
488
|
+
if (typeof source.clear === 'function') {
|
|
489
|
+
source.clear(true);
|
|
490
|
+
}
|
|
491
|
+
source.dispose();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
465
495
|
layer.dispose();
|
|
466
496
|
this.layerCache.delete(id);
|
|
467
497
|
this.updateLayerState();
|
|
@@ -580,6 +610,11 @@ class OlLayerService {
|
|
|
580
610
|
const layer = this.layerCache.get(id);
|
|
581
611
|
if (!(layer instanceof VectorLayer))
|
|
582
612
|
return;
|
|
613
|
+
const sourceProj = layer.get('coordinate-projection') ?? 'EPSG:4326';
|
|
614
|
+
const map = this.mapService.getMap();
|
|
615
|
+
const targetProj = (map && typeof map.getView === 'function'
|
|
616
|
+
? map.getView()?.getProjection()?.getCode()
|
|
617
|
+
: undefined) ?? 'EPSG:3857';
|
|
583
618
|
const source = layer.getSource();
|
|
584
619
|
if (!source)
|
|
585
620
|
return;
|
|
@@ -605,7 +640,10 @@ class OlLayerService {
|
|
|
605
640
|
const featuresToAdd = features.filter((f) => !existingIds.has(f.id));
|
|
606
641
|
if (featuresToAdd.length > 0) {
|
|
607
642
|
const olFeatures = featuresToAdd.map((f) => {
|
|
608
|
-
const olf = featureToOlFeature(f
|
|
643
|
+
const olf = featureToOlFeature(f, {
|
|
644
|
+
sourceProjection: sourceProj,
|
|
645
|
+
targetProjection: targetProj,
|
|
646
|
+
});
|
|
609
647
|
if (f.style)
|
|
610
648
|
olf.set(STYLE_PROP, f.style);
|
|
611
649
|
return olf;
|
|
@@ -674,6 +712,7 @@ class OlVectorLayerComponent {
|
|
|
674
712
|
style = input(...(ngDevMode ? [undefined, { debugName: "style" }] : /* istanbul ignore next */ []));
|
|
675
713
|
cluster = input(...(ngDevMode ? [undefined, { debugName: "cluster" }] : /* istanbul ignore next */ []));
|
|
676
714
|
clusterComponent = contentChild(OlClusterComponent, ...(ngDevMode ? [{ debugName: "clusterComponent" }] : /* istanbul ignore next */ []));
|
|
715
|
+
coordinateProjection = input('EPSG:4326', ...(ngDevMode ? [{ debugName: "coordinateProjection" }] : /* istanbul ignore next */ []));
|
|
677
716
|
constructor() {
|
|
678
717
|
// Initialize layer after DOM is ready
|
|
679
718
|
afterNextRender(() => {
|
|
@@ -701,6 +740,7 @@ class OlVectorLayerComponent {
|
|
|
701
740
|
visible: this.visible(),
|
|
702
741
|
style: this.style(),
|
|
703
742
|
cluster: resolvedClusterConfig,
|
|
743
|
+
coordinateProjection: this.coordinateProjection(),
|
|
704
744
|
});
|
|
705
745
|
});
|
|
706
746
|
// Effect to sync features when input changes
|
|
@@ -737,7 +777,7 @@ class OlVectorLayerComponent {
|
|
|
737
777
|
});
|
|
738
778
|
}
|
|
739
779
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlVectorLayerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
740
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.13", 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 }, url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", 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 } }, queries: [{ propertyName: "clusterComponent", first: true, predicate: OlClusterComponent, descendants: true, isSignal: true }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
780
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.13", 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 }, url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: false, transformFunction: null }, format: { classPropertyName: "format", publicName: "format", 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 }, coordinateProjection: { classPropertyName: "coordinateProjection", publicName: "coordinateProjection", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "clusterComponent", first: true, predicate: OlClusterComponent, descendants: true, isSignal: true }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
741
781
|
}
|
|
742
782
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: OlVectorLayerComponent, decorators: [{
|
|
743
783
|
type: Component,
|
|
@@ -746,7 +786,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImpo
|
|
|
746
786
|
template: '',
|
|
747
787
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
748
788
|
}]
|
|
749
|
-
}], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], features: [{ type: i0.Input, args: [{ isSignal: true, alias: "features", required: false }] }], url: [{ type: i0.Input, args: [{ isSignal: true, alias: "url", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", 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 }] }], clusterComponent: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlClusterComponent), { isSignal: true }] }] } });
|
|
789
|
+
}], ctorParameters: () => [], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], features: [{ type: i0.Input, args: [{ isSignal: true, alias: "features", required: false }] }], url: [{ type: i0.Input, args: [{ isSignal: true, alias: "url", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", 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 }] }], clusterComponent: [{ type: i0.ContentChild, args: [i0.forwardRef(() => OlClusterComponent), { isSignal: true }] }], coordinateProjection: [{ type: i0.Input, args: [{ isSignal: true, alias: "coordinateProjection", required: false }] }] } });
|
|
750
790
|
|
|
751
791
|
// OlTileLayerComponent
|
|
752
792
|
class OlTileLayerComponent {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-helpers/openlayers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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": {
|
|
@@ -231,5 +231,55 @@ declare class OlControlService {
|
|
|
231
231
|
declare function withControls(): OlFeature<'controls'>;
|
|
232
232
|
declare function provideControls(): OlFeature<'controls'>;
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
type TimelinePosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A premium, reactive visual timeline component for OpenLayers.
|
|
238
|
+
* Orchestrates time-series animations by binding directly to OlTimeService.
|
|
239
|
+
*
|
|
240
|
+
* Uses native controls, CSS grid/flex layouts, and a sleek glassmorphic theme.
|
|
241
|
+
* Completely free of CommonModule or FormsModule dependencies for maximum performance.
|
|
242
|
+
*
|
|
243
|
+
* @usageNotes
|
|
244
|
+
* ```html
|
|
245
|
+
* <ol-timeline
|
|
246
|
+
* [startTime]="1700000000000"
|
|
247
|
+
* [endTime]="1700086400000"
|
|
248
|
+
* [playSpeed]="60"
|
|
249
|
+
* [loop]="true"
|
|
250
|
+
* position="bottom-center">
|
|
251
|
+
* </ol-timeline>
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
declare class OlTimelineComponent {
|
|
255
|
+
private timeService;
|
|
256
|
+
/** Start bounds of time-series (Epoch ms) */
|
|
257
|
+
startTime: _angular_core.InputSignal<number>;
|
|
258
|
+
/** End bounds of time-series (Epoch ms) */
|
|
259
|
+
endTime: _angular_core.InputSignal<number>;
|
|
260
|
+
/** Default speed multiplier (e.g. 1, 5, 10, 60, 3600) */
|
|
261
|
+
playSpeed: _angular_core.InputSignal<number>;
|
|
262
|
+
/** Loop playback when reaching endTime */
|
|
263
|
+
loop: _angular_core.InputSignal<boolean>;
|
|
264
|
+
/** Position overlay alignment */
|
|
265
|
+
position: _angular_core.InputSignal<TimelinePosition>;
|
|
266
|
+
/** Custom label formatter */
|
|
267
|
+
formatLabel: _angular_core.InputSignal<(time: number) => string>;
|
|
268
|
+
/** Outputs */
|
|
269
|
+
timeChange: _angular_core.OutputEmitterRef<number>;
|
|
270
|
+
playStateChange: _angular_core.OutputEmitterRef<boolean>;
|
|
271
|
+
/** Computeds binding directly to OlTimeService */
|
|
272
|
+
currentTime: _angular_core.Signal<number>;
|
|
273
|
+
isPlaying: _angular_core.Signal<boolean>;
|
|
274
|
+
speed: _angular_core.Signal<number>;
|
|
275
|
+
formattedTime: _angular_core.Signal<string>;
|
|
276
|
+
constructor();
|
|
277
|
+
togglePlay(): void;
|
|
278
|
+
onScrub(event: Event): void;
|
|
279
|
+
onSpeedChange(event: Event): void;
|
|
280
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlTimelineComponent, never>;
|
|
281
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlTimelineComponent, "ol-timeline", never, { "startTime": { "alias": "startTime"; "required": true; "isSignal": true; }; "endTime": { "alias": "endTime"; "required": true; "isSignal": true; }; "playSpeed": { "alias": "playSpeed"; "required": false; "isSignal": true; }; "loop": { "alias": "loop"; "required": false; "isSignal": true; }; "position": { "alias": "position"; "required": false; "isSignal": true; }; "formatLabel": { "alias": "formatLabel"; "required": false; "isSignal": true; }; }, { "timeChange": "timeChange"; "playStateChange": "playStateChange"; }, never, never, true, never>;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export { OlAttributionControlComponent, OlBasemapSwitcherComponent, OlControlService, OlFullscreenControlComponent, OlGeolocationControlComponent, OlLayerSwitcherComponent, OlRotateControlComponent, OlScaleLineControlComponent, OlTimelineComponent, OlZoomControlComponent, ROTATE_CONTROL_MAP_SERVICE, provideControls, withControls };
|
|
285
|
+
export type { BasemapConfig, BasemapSwitcherPosition, ControlConfig, ControlPosition, LayerSwitcherItem, LayerSwitcherPosition, RotateControlMapService, TimelinePosition };
|
|
@@ -94,6 +94,7 @@ declare class OlMapComponent {
|
|
|
94
94
|
zoom: _angular_core.InputSignal<number>;
|
|
95
95
|
rotation: _angular_core.InputSignal<number>;
|
|
96
96
|
projection: _angular_core.InputSignal<string>;
|
|
97
|
+
coordinateProjection: _angular_core.InputSignal<string>;
|
|
97
98
|
viewChange: _angular_core.OutputEmitterRef<ViewState>;
|
|
98
99
|
mapClick: _angular_core.OutputEmitterRef<MapClickEvent>;
|
|
99
100
|
mapDblClick: _angular_core.OutputEmitterRef<MapClickEvent>;
|
|
@@ -101,6 +102,8 @@ declare class OlMapComponent {
|
|
|
101
102
|
private map?;
|
|
102
103
|
private resizeObserver?;
|
|
103
104
|
constructor();
|
|
105
|
+
private getProjectedCoordinate;
|
|
106
|
+
private getExternalCoordinate;
|
|
104
107
|
private initMap;
|
|
105
108
|
private destroyMap;
|
|
106
109
|
private updateCenter;
|
|
@@ -108,7 +111,7 @@ declare class OlMapComponent {
|
|
|
108
111
|
private updateRotation;
|
|
109
112
|
private emitViewChange;
|
|
110
113
|
static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlMapComponent, never>;
|
|
111
|
-
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlMapComponent, "ol-map", never, { "center": { "alias": "center"; "required": false; "isSignal": true; }; "zoom": { "alias": "zoom"; "required": false; "isSignal": true; }; "rotation": { "alias": "rotation"; "required": false; "isSignal": true; }; "projection": { "alias": "projection"; "required": false; "isSignal": true; }; }, { "viewChange": "viewChange"; "mapClick": "mapClick"; "mapDblClick": "mapDblClick"; }, never, ["*"], true, never>;
|
|
114
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlMapComponent, "ol-map", never, { "center": { "alias": "center"; "required": false; "isSignal": true; }; "zoom": { "alias": "zoom"; "required": false; "isSignal": true; }; "rotation": { "alias": "rotation"; "required": false; "isSignal": true; }; "projection": { "alias": "projection"; "required": false; "isSignal": true; }; "coordinateProjection": { "alias": "coordinateProjection"; "required": false; "isSignal": true; }; }, { "viewChange": "viewChange"; "mapClick": "mapClick"; "mapDblClick": "mapDblClick"; }, never, ["*"], true, never>;
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/**
|
|
@@ -379,18 +382,23 @@ interface VectorResourceOptions {
|
|
|
379
382
|
*/
|
|
380
383
|
declare function createVectorResource(url: Signal<string | undefined>, options?: VectorResourceOptions): Resource<Feature[]>;
|
|
381
384
|
|
|
385
|
+
interface ProjectionOptions {
|
|
386
|
+
sourceProjection?: string;
|
|
387
|
+
targetProjection?: string;
|
|
388
|
+
}
|
|
382
389
|
/**
|
|
383
390
|
* Converts an OpenLayers feature to the internal Feature format.
|
|
384
|
-
* Handles coordinate extraction and geometry type mapping.
|
|
391
|
+
* Handles coordinate extraction and geometry type mapping with custom projections.
|
|
385
392
|
*
|
|
386
393
|
* @param olFeature - The OpenLayers feature to convert
|
|
394
|
+
* @param options - Projection source and target codes
|
|
387
395
|
* @returns The converted Feature with normalized structure
|
|
388
396
|
*/
|
|
389
|
-
declare function olFeatureToFeature(olFeature: Feature$1): Feature;
|
|
397
|
+
declare function olFeatureToFeature(olFeature: Feature$1, options?: ProjectionOptions): Feature;
|
|
390
398
|
/**
|
|
391
399
|
* Converts an internal Feature to an OpenLayers feature.
|
|
392
400
|
*/
|
|
393
|
-
declare function featureToOlFeature(feature: Feature): Feature$1;
|
|
401
|
+
declare function featureToOlFeature(feature: Feature, options?: ProjectionOptions): Feature$1;
|
|
394
402
|
|
|
395
403
|
type OlFeatureKind = 'layers' | 'controls' | 'interactions' | 'overlays' | 'military' | 'projections';
|
|
396
404
|
interface OlFeature<Kind extends OlFeatureKind> {
|
|
@@ -33,6 +33,7 @@ interface VectorLayerConfig extends LayerConfig {
|
|
|
33
33
|
format?: 'geojson' | 'topojson' | 'kml';
|
|
34
34
|
style?: Style | ((feature: Feature) => Style);
|
|
35
35
|
cluster?: ClusterConfig;
|
|
36
|
+
coordinateProjection?: string;
|
|
36
37
|
}
|
|
37
38
|
interface HeatmapLayerConfig extends LayerConfig {
|
|
38
39
|
type: 'heatmap';
|
|
@@ -87,9 +88,10 @@ declare class OlVectorLayerComponent {
|
|
|
87
88
|
style: _angular_core.InputSignal<any>;
|
|
88
89
|
cluster: _angular_core.InputSignal<ClusterConfig>;
|
|
89
90
|
clusterComponent: _angular_core.Signal<OlClusterComponent>;
|
|
91
|
+
coordinateProjection: _angular_core.InputSignal<string>;
|
|
90
92
|
constructor();
|
|
91
93
|
static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlVectorLayerComponent, never>;
|
|
92
|
-
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlVectorLayerComponent, "ol-vector-layer", never, { "id": { "alias": "id"; "required": true; "isSignal": true; }; "features": { "alias": "features"; "required": false; "isSignal": true; }; "url": { "alias": "url"; "required": false; "isSignal": true; }; "format": { "alias": "format"; "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; }; }, {}, ["clusterComponent"], never, true, never>;
|
|
94
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlVectorLayerComponent, "ol-vector-layer", never, { "id": { "alias": "id"; "required": true; "isSignal": true; }; "features": { "alias": "features"; "required": false; "isSignal": true; }; "url": { "alias": "url"; "required": false; "isSignal": true; }; "format": { "alias": "format"; "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; }; "coordinateProjection": { "alias": "coordinateProjection"; "required": false; "isSignal": true; }; }, {}, ["clusterComponent"], never, true, never>;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
declare class OlTileLayerComponent {
|