@angular-helpers/openlayers 0.6.0 → 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 CHANGED
@@ -276,6 +276,68 @@ Renders points, lines, and polygons using WebGL 2. For peak performance, hit det
276
276
 
277
277
  Rigorous cleanup guarantees that WebGL contexts, framebuffers, and active buffers are fully released on destroy (`layer.dispose()`), preventing GPU leaks.
278
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
+
279
341
  ## Geodesic Geometry Helpers
280
342
 
281
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-helpers/openlayers",
3
- "version": "0.6.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
- export { OlAttributionControlComponent, OlBasemapSwitcherComponent, OlControlService, OlFullscreenControlComponent, OlGeolocationControlComponent, OlLayerSwitcherComponent, OlRotateControlComponent, OlScaleLineControlComponent, OlZoomControlComponent, ROTATE_CONTROL_MAP_SERVICE, provideControls, withControls };
235
- export type { BasemapConfig, BasemapSwitcherPosition, ControlConfig, ControlPosition, LayerSwitcherItem, LayerSwitcherPosition, RotateControlMapService };
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 };