@kteneyck/cesium-timeline-angular 0.1.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.
@@ -0,0 +1,1573 @@
1
+ import * as i0 from '@angular/core';
2
+ import { EventEmitter, ViewChild, Output, Input, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import * as Cesium from 'cesium';
4
+ import { splitForDisplay, formatDateTime, MIN_SPAN_MS, MAX_SPAN_MS, drawTimeline, hitTestSwimLane, hitTestLaneLabel, isInSwimLaneRegion, zoomRange, DEFAULT_LANE_HEIGHT, LANE_GAP, totalSwimLaneHeight, TICK_AREA_HEIGHT, SWIM_LANE_SCROLL_SPEED, defaultTheme, toJulianDate } from '@kteneyck/cesium-timeline-core';
5
+ export * from '@kteneyck/cesium-timeline-core';
6
+ export { TICK_AREA_HEIGHT } from '@kteneyck/cesium-timeline-core';
7
+
8
+ class TimelineControlsComponent {
9
+ currentTime;
10
+ isPlaying = false;
11
+ multiplier = 1;
12
+ dateTimeFormat;
13
+ isLive = false;
14
+ hasStartTime = false;
15
+ hasEndTime = false;
16
+ theme;
17
+ swimLanesVisible;
18
+ dateTimeClick = new EventEmitter();
19
+ playPause = new EventEmitter();
20
+ jumpToStart = new EventEmitter();
21
+ rewind = new EventEmitter();
22
+ fastForward = new EventEmitter();
23
+ jumpToEnd = new EventEmitter();
24
+ jumpToLive = new EventEmitter();
25
+ resetSpeed = new EventEmitter();
26
+ toggleSwimLanes = new EventEmitter();
27
+ containerRef;
28
+ isNarrow = false;
29
+ ro;
30
+ get isRewinding() { return this.multiplier < 0; }
31
+ get isFastForward() { return this.multiplier > 1; }
32
+ get isNormalSpeed() { return this.multiplier === 1; }
33
+ get absMultiplier() { return Math.abs(this.multiplier); }
34
+ get hasSwimLaneToggle() { return this.swimLanesVisible != null; }
35
+ get timeFormat() { return splitForDisplay(this.dateTimeFormat).timeFormat; }
36
+ get dateFormat() { return splitForDisplay(this.dateTimeFormat).dateFormat; }
37
+ get formattedTime() { return formatDateTime(this.currentTime, this.timeFormat); }
38
+ get formattedDate() { return formatDateTime(this.currentTime, this.dateFormat); }
39
+ ngAfterViewInit() {
40
+ const el = this.containerRef?.nativeElement;
41
+ if (!el)
42
+ return;
43
+ this.ro = new ResizeObserver(([entry]) => {
44
+ this.isNarrow = entry.contentRect.width < 520;
45
+ });
46
+ this.ro.observe(el);
47
+ }
48
+ ngOnDestroy() {
49
+ this.ro?.disconnect();
50
+ }
51
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: TimelineControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
52
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: TimelineControlsComponent, isStandalone: true, selector: "ct-timeline-controls", inputs: { currentTime: "currentTime", isPlaying: "isPlaying", multiplier: "multiplier", dateTimeFormat: "dateTimeFormat", isLive: "isLive", hasStartTime: "hasStartTime", hasEndTime: "hasEndTime", theme: "theme", swimLanesVisible: "swimLanesVisible" }, outputs: { dateTimeClick: "dateTimeClick", playPause: "playPause", jumpToStart: "jumpToStart", rewind: "rewind", fastForward: "fastForward", jumpToEnd: "jumpToEnd", jumpToLive: "jumpToLive", resetSpeed: "resetSpeed", toggleSwimLanes: "toggleSwimLanes" }, viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["container"], descendants: true }], ngImport: i0, template: `
53
+ <div
54
+ #container
55
+ [style.display]="isNarrow ? 'flex' : 'grid'"
56
+ [style.grid-template-columns]="isNarrow ? undefined : '1fr auto 1fr'"
57
+ [style.align-items]="'center'"
58
+ [style.padding]="'6px 16px'"
59
+ [style.background-color]="theme.controlBarBackground"
60
+ [style.border-bottom]="'1px solid ' + theme.controlBarBorder"
61
+ [style.font-family]="'system-ui, -apple-system, sans-serif'"
62
+ >
63
+ <!-- Left: Datetime + LIVE + speed badge -->
64
+ <div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
65
+ <div
66
+ (click)="dateTimeClick.emit()"
67
+ [title]="dateTimeClick.observed ? 'Click to jump to a date/time' : ''"
68
+ [style.color]="theme.labelColor"
69
+ style="font-family:monospace;line-height:1.15;border-radius:4px;padding:2px 4px;transition:background 0.15s"
70
+ [style.cursor]="dateTimeClick.observed ? 'pointer' : 'default'"
71
+ >
72
+ @if (timeFormat) {
73
+ <div style="font-size:2em;font-weight:bold;letter-spacing:0.02em">
74
+ {{ formattedTime }}
75
+ </div>
76
+ }
77
+ @if (dateFormat) {
78
+ <div [style.color]="theme.buttonActiveColor" style="font-size:1.15em;letter-spacing:0.03em">
79
+ {{ formattedDate }}
80
+ </div>
81
+ }
82
+ </div>
83
+
84
+ <div style="display:flex;flex-direction:column;gap:2px;justify-content:center">
85
+ <!-- LIVE button -->
86
+ <button
87
+ (click)="jumpToLive.emit()"
88
+ [style.color]="isLive ? theme.controlBarBackground : theme.buttonActiveColor"
89
+ [style.background-color]="isLive ? theme.buttonActiveColor : 'transparent'"
90
+ [style.border-color]="theme.buttonActiveColor"
91
+ [style.opacity]="isLive ? 1 : 0.55"
92
+ style="background:none;border:1px solid;cursor:pointer;font-size:11px;font-weight:bold;letter-spacing:0.05em;width:52px;min-width:52px;height:20px;border-radius:3px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:opacity 0.15s"
93
+ [title]="isLive ? 'Currently live' : 'Jump to live (now)'"
94
+ >
95
+ {{ isLive ? '● LIVE' : 'LIVE' }}
96
+ </button>
97
+
98
+ <!-- Speed badge -->
99
+ <div style="height:20px;display:flex;align-items:center">
100
+ @if (!isNormalSpeed) {
101
+ <button
102
+ (click)="resetSpeed.emit()"
103
+ [style.color]="theme.buttonActiveColor"
104
+ [style.border-color]="theme.buttonActiveColor + '44'"
105
+ style="background:none;border:1px solid;cursor:pointer;font-size:11px;width:52px;min-width:52px;height:20px;border-radius:4px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:background-color 0.15s"
106
+ title="Reset to 1× speed"
107
+ >
108
+ {{ isRewinding ? '◀ ' + absMultiplier + '×' : absMultiplier + '× ▶' }}
109
+ </button>
110
+ }
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ <!-- Center: Transport buttons -->
116
+ <div
117
+ style="display:flex;align-items:center;gap:2px"
118
+ [style.flex]="isNarrow ? '1' : undefined"
119
+ [style.justify-content]="isNarrow ? 'center' : undefined"
120
+ >
121
+ @if (hasStartTime) {
122
+ <button
123
+ (click)="jumpToStart.emit()"
124
+ [style.color]="theme.buttonColor"
125
+ class="ct-btn"
126
+ title="Jump to start"
127
+ >⏮</button>
128
+ }
129
+
130
+ <button
131
+ (click)="rewind.emit()"
132
+ [style.color]="isRewinding ? theme.buttonActiveColor : theme.buttonColor"
133
+ [style.border-color]="isRewinding ? theme.buttonActiveColor + '33' : 'transparent'"
134
+ class="ct-btn ct-btn-wide"
135
+ [title]="isRewinding ? 'Reverse ' + absMultiplier + '× — click to speed up' : 'Rewind'"
136
+ >
137
+ @if (isRewinding) {
138
+ <span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>◀◀
139
+ } @else {
140
+ ◀◀
141
+ }
142
+ </button>
143
+
144
+ <button
145
+ (click)="playPause.emit(!isPlaying)"
146
+ [style.color]="theme.buttonActiveColor"
147
+ [style.border-color]="theme.buttonActiveColor + '55'"
148
+ [style.padding-left]="isPlaying ? '0' : '2px'"
149
+ class="ct-btn ct-btn-play"
150
+ [title]="isPlaying ? 'Pause' : (isRewinding ? 'Play (reset to 1×)' : 'Play')"
151
+ >
152
+ @if (isPlaying) {
153
+ <svg width="14" height="16" viewBox="0 0 14 16" fill="currentColor">
154
+ <rect x="1" y="0" width="4" height="16" rx="1"/>
155
+ <rect x="9" y="0" width="4" height="16" rx="1"/>
156
+ </svg>
157
+ } @else {
158
+
159
+ }
160
+ </button>
161
+
162
+ <button
163
+ (click)="fastForward.emit()"
164
+ [style.color]="isFastForward ? theme.buttonActiveColor : theme.buttonColor"
165
+ [style.border-color]="isFastForward ? theme.buttonActiveColor + '33' : 'transparent'"
166
+ class="ct-btn ct-btn-wide"
167
+ [title]="isFastForward ? absMultiplier + '× speed — click to increase' : 'Fast forward'"
168
+ >
169
+ @if (isFastForward) {
170
+ ▶▶<span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>
171
+ } @else {
172
+ ▶▶
173
+ }
174
+ </button>
175
+
176
+ @if (hasEndTime) {
177
+ <button
178
+ (click)="jumpToEnd.emit()"
179
+ [style.color]="theme.buttonColor"
180
+ class="ct-btn"
181
+ title="Jump to end"
182
+ >⏭</button>
183
+ }
184
+ </div>
185
+
186
+ <!-- Right: swim-lane toggle -->
187
+ @if (!isNarrow) {
188
+ <div style="display:flex;justify-content:flex-end;align-items:center">
189
+ @if (hasSwimLaneToggle) {
190
+ <button
191
+ (click)="toggleSwimLanes.emit()"
192
+ [style.color]="theme.buttonActiveColor"
193
+ [style.border-color]="theme.buttonActiveColor + '33'"
194
+ class="ct-btn"
195
+ [title]="swimLanesVisible ? 'Collapse swim lanes' : 'Expand swim lanes'"
196
+ >
197
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
198
+ @if (swimLanesVisible) {
199
+ <polyline points="3,5 7,9 11,5"/>
200
+ } @else {
201
+ <polyline points="3,9 7,5 11,9"/>
202
+ }
203
+ </svg>
204
+ </button>
205
+ }
206
+ </div>
207
+ }
208
+
209
+ @if (isNarrow && hasSwimLaneToggle) {
210
+ <button
211
+ (click)="toggleSwimLanes.emit()"
212
+ [style.color]="theme.buttonActiveColor"
213
+ [style.border-color]="theme.buttonActiveColor + '33'"
214
+ class="ct-btn"
215
+ style="margin-left:4px"
216
+ [title]="swimLanesVisible ? 'Collapse swim lanes' : 'Expand swim lanes'"
217
+ >
218
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
219
+ @if (swimLanesVisible) {
220
+ <polyline points="3,5 7,9 11,5"/>
221
+ } @else {
222
+ <polyline points="3,9 7,5 11,9"/>
223
+ }
224
+ </svg>
225
+ </button>
226
+ }
227
+ </div>
228
+ `, isInline: true, styles: [":host{display:block}.ct-btn{background:none;border:1px solid transparent;cursor:pointer;font-size:16px;padding:0;display:flex;align-items:center;justify-content:center;min-width:32px;width:32px;height:32px;border-radius:4px;transition:background-color .15s,color .15s;font-family:system-ui,-apple-system,sans-serif;flex-shrink:0;line-height:1}.ct-btn:hover{background-color:#ffffff1a}.ct-btn-wide{width:64px;min-width:64px;gap:3px}.ct-btn-play{font-size:18px;width:40px;min-width:40px;height:40px;border-radius:50%}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
229
+ }
230
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: TimelineControlsComponent, decorators: [{
231
+ type: Component,
232
+ args: [{ selector: 'ct-timeline-controls', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
233
+ <div
234
+ #container
235
+ [style.display]="isNarrow ? 'flex' : 'grid'"
236
+ [style.grid-template-columns]="isNarrow ? undefined : '1fr auto 1fr'"
237
+ [style.align-items]="'center'"
238
+ [style.padding]="'6px 16px'"
239
+ [style.background-color]="theme.controlBarBackground"
240
+ [style.border-bottom]="'1px solid ' + theme.controlBarBorder"
241
+ [style.font-family]="'system-ui, -apple-system, sans-serif'"
242
+ >
243
+ <!-- Left: Datetime + LIVE + speed badge -->
244
+ <div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
245
+ <div
246
+ (click)="dateTimeClick.emit()"
247
+ [title]="dateTimeClick.observed ? 'Click to jump to a date/time' : ''"
248
+ [style.color]="theme.labelColor"
249
+ style="font-family:monospace;line-height:1.15;border-radius:4px;padding:2px 4px;transition:background 0.15s"
250
+ [style.cursor]="dateTimeClick.observed ? 'pointer' : 'default'"
251
+ >
252
+ @if (timeFormat) {
253
+ <div style="font-size:2em;font-weight:bold;letter-spacing:0.02em">
254
+ {{ formattedTime }}
255
+ </div>
256
+ }
257
+ @if (dateFormat) {
258
+ <div [style.color]="theme.buttonActiveColor" style="font-size:1.15em;letter-spacing:0.03em">
259
+ {{ formattedDate }}
260
+ </div>
261
+ }
262
+ </div>
263
+
264
+ <div style="display:flex;flex-direction:column;gap:2px;justify-content:center">
265
+ <!-- LIVE button -->
266
+ <button
267
+ (click)="jumpToLive.emit()"
268
+ [style.color]="isLive ? theme.controlBarBackground : theme.buttonActiveColor"
269
+ [style.background-color]="isLive ? theme.buttonActiveColor : 'transparent'"
270
+ [style.border-color]="theme.buttonActiveColor"
271
+ [style.opacity]="isLive ? 1 : 0.55"
272
+ style="background:none;border:1px solid;cursor:pointer;font-size:11px;font-weight:bold;letter-spacing:0.05em;width:52px;min-width:52px;height:20px;border-radius:3px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:opacity 0.15s"
273
+ [title]="isLive ? 'Currently live' : 'Jump to live (now)'"
274
+ >
275
+ {{ isLive ? '● LIVE' : 'LIVE' }}
276
+ </button>
277
+
278
+ <!-- Speed badge -->
279
+ <div style="height:20px;display:flex;align-items:center">
280
+ @if (!isNormalSpeed) {
281
+ <button
282
+ (click)="resetSpeed.emit()"
283
+ [style.color]="theme.buttonActiveColor"
284
+ [style.border-color]="theme.buttonActiveColor + '44'"
285
+ style="background:none;border:1px solid;cursor:pointer;font-size:11px;width:52px;min-width:52px;height:20px;border-radius:4px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:background-color 0.15s"
286
+ title="Reset to 1× speed"
287
+ >
288
+ {{ isRewinding ? '◀ ' + absMultiplier + '×' : absMultiplier + '× ▶' }}
289
+ </button>
290
+ }
291
+ </div>
292
+ </div>
293
+ </div>
294
+
295
+ <!-- Center: Transport buttons -->
296
+ <div
297
+ style="display:flex;align-items:center;gap:2px"
298
+ [style.flex]="isNarrow ? '1' : undefined"
299
+ [style.justify-content]="isNarrow ? 'center' : undefined"
300
+ >
301
+ @if (hasStartTime) {
302
+ <button
303
+ (click)="jumpToStart.emit()"
304
+ [style.color]="theme.buttonColor"
305
+ class="ct-btn"
306
+ title="Jump to start"
307
+ >⏮</button>
308
+ }
309
+
310
+ <button
311
+ (click)="rewind.emit()"
312
+ [style.color]="isRewinding ? theme.buttonActiveColor : theme.buttonColor"
313
+ [style.border-color]="isRewinding ? theme.buttonActiveColor + '33' : 'transparent'"
314
+ class="ct-btn ct-btn-wide"
315
+ [title]="isRewinding ? 'Reverse ' + absMultiplier + '× — click to speed up' : 'Rewind'"
316
+ >
317
+ @if (isRewinding) {
318
+ <span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>◀◀
319
+ } @else {
320
+ ◀◀
321
+ }
322
+ </button>
323
+
324
+ <button
325
+ (click)="playPause.emit(!isPlaying)"
326
+ [style.color]="theme.buttonActiveColor"
327
+ [style.border-color]="theme.buttonActiveColor + '55'"
328
+ [style.padding-left]="isPlaying ? '0' : '2px'"
329
+ class="ct-btn ct-btn-play"
330
+ [title]="isPlaying ? 'Pause' : (isRewinding ? 'Play (reset to 1×)' : 'Play')"
331
+ >
332
+ @if (isPlaying) {
333
+ <svg width="14" height="16" viewBox="0 0 14 16" fill="currentColor">
334
+ <rect x="1" y="0" width="4" height="16" rx="1"/>
335
+ <rect x="9" y="0" width="4" height="16" rx="1"/>
336
+ </svg>
337
+ } @else {
338
+
339
+ }
340
+ </button>
341
+
342
+ <button
343
+ (click)="fastForward.emit()"
344
+ [style.color]="isFastForward ? theme.buttonActiveColor : theme.buttonColor"
345
+ [style.border-color]="isFastForward ? theme.buttonActiveColor + '33' : 'transparent'"
346
+ class="ct-btn ct-btn-wide"
347
+ [title]="isFastForward ? absMultiplier + '× speed — click to increase' : 'Fast forward'"
348
+ >
349
+ @if (isFastForward) {
350
+ ▶▶<span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>
351
+ } @else {
352
+ ▶▶
353
+ }
354
+ </button>
355
+
356
+ @if (hasEndTime) {
357
+ <button
358
+ (click)="jumpToEnd.emit()"
359
+ [style.color]="theme.buttonColor"
360
+ class="ct-btn"
361
+ title="Jump to end"
362
+ >⏭</button>
363
+ }
364
+ </div>
365
+
366
+ <!-- Right: swim-lane toggle -->
367
+ @if (!isNarrow) {
368
+ <div style="display:flex;justify-content:flex-end;align-items:center">
369
+ @if (hasSwimLaneToggle) {
370
+ <button
371
+ (click)="toggleSwimLanes.emit()"
372
+ [style.color]="theme.buttonActiveColor"
373
+ [style.border-color]="theme.buttonActiveColor + '33'"
374
+ class="ct-btn"
375
+ [title]="swimLanesVisible ? 'Collapse swim lanes' : 'Expand swim lanes'"
376
+ >
377
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
378
+ @if (swimLanesVisible) {
379
+ <polyline points="3,5 7,9 11,5"/>
380
+ } @else {
381
+ <polyline points="3,9 7,5 11,9"/>
382
+ }
383
+ </svg>
384
+ </button>
385
+ }
386
+ </div>
387
+ }
388
+
389
+ @if (isNarrow && hasSwimLaneToggle) {
390
+ <button
391
+ (click)="toggleSwimLanes.emit()"
392
+ [style.color]="theme.buttonActiveColor"
393
+ [style.border-color]="theme.buttonActiveColor + '33'"
394
+ class="ct-btn"
395
+ style="margin-left:4px"
396
+ [title]="swimLanesVisible ? 'Collapse swim lanes' : 'Expand swim lanes'"
397
+ >
398
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
399
+ @if (swimLanesVisible) {
400
+ <polyline points="3,5 7,9 11,5"/>
401
+ } @else {
402
+ <polyline points="3,9 7,5 11,9"/>
403
+ }
404
+ </svg>
405
+ </button>
406
+ }
407
+ </div>
408
+ `, styles: [":host{display:block}.ct-btn{background:none;border:1px solid transparent;cursor:pointer;font-size:16px;padding:0;display:flex;align-items:center;justify-content:center;min-width:32px;width:32px;height:32px;border-radius:4px;transition:background-color .15s,color .15s;font-family:system-ui,-apple-system,sans-serif;flex-shrink:0;line-height:1}.ct-btn:hover{background-color:#ffffff1a}.ct-btn-wide{width:64px;min-width:64px;gap:3px}.ct-btn-play{font-size:18px;width:40px;min-width:40px;height:40px;border-radius:50%}\n"] }]
409
+ }], propDecorators: { currentTime: [{
410
+ type: Input
411
+ }], isPlaying: [{
412
+ type: Input
413
+ }], multiplier: [{
414
+ type: Input
415
+ }], dateTimeFormat: [{
416
+ type: Input
417
+ }], isLive: [{
418
+ type: Input
419
+ }], hasStartTime: [{
420
+ type: Input
421
+ }], hasEndTime: [{
422
+ type: Input
423
+ }], theme: [{
424
+ type: Input
425
+ }], swimLanesVisible: [{
426
+ type: Input
427
+ }], dateTimeClick: [{
428
+ type: Output
429
+ }], playPause: [{
430
+ type: Output
431
+ }], jumpToStart: [{
432
+ type: Output
433
+ }], rewind: [{
434
+ type: Output
435
+ }], fastForward: [{
436
+ type: Output
437
+ }], jumpToEnd: [{
438
+ type: Output
439
+ }], jumpToLive: [{
440
+ type: Output
441
+ }], resetSpeed: [{
442
+ type: Output
443
+ }], toggleSwimLanes: [{
444
+ type: Output
445
+ }], containerRef: [{
446
+ type: ViewChild,
447
+ args: ['container']
448
+ }] } });
449
+
450
+ /**
451
+ * TimelineCanvasComponent – Angular wrapper around the core canvas rendering engine.
452
+ *
453
+ * All mutable state lives as class properties (equivalent to React refs) — no
454
+ * change detection cycles are triggered from drawing or mouse handlers.
455
+ * The core drawTimeline() function handles all rendering.
456
+ */
457
+ class TimelineCanvasComponent {
458
+ ngZone;
459
+ currentTime;
460
+ defaultStartMs;
461
+ defaultEndMs;
462
+ theme;
463
+ maxTicks;
464
+ swimLanes;
465
+ showSwimLanes;
466
+ timeChange = new EventEmitter();
467
+ dragStart = new EventEmitter();
468
+ dragEnd = new EventEmitter();
469
+ swimLaneItemClick = new EventEmitter();
470
+ swimLaneItemHover = new EventEmitter();
471
+ swimLaneItemDoubleClick = new EventEmitter();
472
+ swimLaneItemContextMenu = new EventEmitter();
473
+ swimLaneReorder = new EventEmitter();
474
+ canvasRef;
475
+ // ── Mutable render state (equivalent to React refs) ────────────────────
476
+ startMs = 0;
477
+ endMs = 0;
478
+ curMs = 0;
479
+ scrollTop = 0;
480
+ swimLanesState = [];
481
+ showSwimLanesState = false;
482
+ hoveredItem = null;
483
+ reorderState = null;
484
+ // Mouse state
485
+ mouseMode = 'none';
486
+ mouseX = 0;
487
+ scrubClientX = 0;
488
+ swimLaneDownTime = 0;
489
+ // Touch state
490
+ touchMode = 'none';
491
+ touchX = 0;
492
+ pinchDist = 0;
493
+ // Edge-scroll animation
494
+ edgeRAF = null;
495
+ // Follow-scroll (playback auto-scroll)
496
+ followRAF = null;
497
+ following = false;
498
+ followRate = 0;
499
+ ro;
500
+ // Bound handlers for cleanup
501
+ boundMouseMove = this.onDocMouseMove.bind(this);
502
+ boundMouseUp = this.onDocMouseUp.bind(this);
503
+ boundTouchStart = this.onTouchStart.bind(this);
504
+ boundTouchMove = this.onTouchMove.bind(this);
505
+ boundTouchEnd = this.onTouchEnd.bind(this);
506
+ boundWheel = this.onWheel.bind(this);
507
+ constructor(ngZone) {
508
+ this.ngZone = ngZone;
509
+ }
510
+ // ── Lifecycle ──────────────────────────────────────────────────────────
511
+ ngAfterViewInit() {
512
+ this.startMs = this.defaultStartMs;
513
+ this.endMs = this.defaultEndMs;
514
+ this.curMs = Cesium.JulianDate.toDate(this.currentTime).getTime();
515
+ this.swimLanesState = this.swimLanes ?? [];
516
+ this.showSwimLanesState = this.showSwimLanes ?? (this.swimLanesState.length > 0);
517
+ // Run outside Angular zone for performance — no change detection on draw/mouse
518
+ this.ngZone.runOutsideAngular(() => {
519
+ this.draw();
520
+ const canvas = this.canvasRef.nativeElement;
521
+ this.ro = new ResizeObserver(() => this.draw());
522
+ this.ro.observe(canvas);
523
+ document.addEventListener('mousemove', this.boundMouseMove);
524
+ document.addEventListener('mouseup', this.boundMouseUp);
525
+ canvas.addEventListener('wheel', this.boundWheel, { passive: false });
526
+ canvas.addEventListener('touchstart', this.boundTouchStart, { passive: false });
527
+ canvas.addEventListener('touchmove', this.boundTouchMove, { passive: false });
528
+ canvas.addEventListener('touchend', this.boundTouchEnd, { passive: false });
529
+ });
530
+ }
531
+ ngOnChanges(changes) {
532
+ if (changes['currentTime'] && !this.following) {
533
+ const newMs = Cesium.JulianDate.toDate(this.currentTime).getTime();
534
+ if (this.curMs !== newMs) {
535
+ this.curMs = newMs;
536
+ this.draw();
537
+ }
538
+ }
539
+ if (changes['theme'] || changes['maxTicks']) {
540
+ this.draw();
541
+ }
542
+ if (changes['swimLanes']) {
543
+ this.swimLanesState = this.swimLanes ?? [];
544
+ this.draw();
545
+ }
546
+ if (changes['showSwimLanes']) {
547
+ this.showSwimLanesState = this.showSwimLanes ?? (this.swimLanesState.length > 0);
548
+ this.draw();
549
+ }
550
+ }
551
+ ngOnDestroy() {
552
+ this.ro?.disconnect();
553
+ document.removeEventListener('mousemove', this.boundMouseMove);
554
+ document.removeEventListener('mouseup', this.boundMouseUp);
555
+ const canvas = this.canvasRef?.nativeElement;
556
+ if (canvas) {
557
+ canvas.removeEventListener('wheel', this.boundWheel);
558
+ canvas.removeEventListener('touchstart', this.boundTouchStart);
559
+ canvas.removeEventListener('touchmove', this.boundTouchMove);
560
+ canvas.removeEventListener('touchend', this.boundTouchEnd);
561
+ }
562
+ if (this.edgeRAF !== null)
563
+ cancelAnimationFrame(this.edgeRAF);
564
+ if (this.followRAF !== null)
565
+ cancelAnimationFrame(this.followRAF);
566
+ }
567
+ // ── Public API (called by parent via ViewChild) ────────────────────────
568
+ zoomTo(startMs, endMs, currentMs) {
569
+ const span = Math.max(MIN_SPAN_MS, Math.min(MAX_SPAN_MS, endMs - startMs));
570
+ const center = (startMs + endMs) / 2;
571
+ this.startMs = center - span / 2;
572
+ this.endMs = center + span / 2;
573
+ if (currentMs !== undefined)
574
+ this.curMs = currentMs;
575
+ this.draw();
576
+ }
577
+ getVisibleRange() {
578
+ return { startMs: this.startMs, endMs: this.endMs };
579
+ }
580
+ startFollow(rate) {
581
+ this.followRate = rate;
582
+ if (this.followRAF !== null)
583
+ return;
584
+ this.following = true;
585
+ let lastTime = performance.now();
586
+ const scroll = () => {
587
+ const now = performance.now();
588
+ const dt = now - lastTime;
589
+ lastTime = now;
590
+ const shift = dt * this.followRate;
591
+ this.startMs += shift;
592
+ this.endMs += shift;
593
+ this.curMs += shift;
594
+ this.draw();
595
+ this.followRAF = requestAnimationFrame(scroll);
596
+ };
597
+ this.followRAF = requestAnimationFrame(scroll);
598
+ }
599
+ stopFollow() {
600
+ this.following = false;
601
+ if (this.followRAF !== null) {
602
+ cancelAnimationFrame(this.followRAF);
603
+ this.followRAF = null;
604
+ }
605
+ }
606
+ correctFollow(currentMs) {
607
+ if (!this.following)
608
+ return;
609
+ const drift = currentMs - this.curMs;
610
+ this.curMs = currentMs;
611
+ this.startMs += drift;
612
+ this.endMs += drift;
613
+ }
614
+ appendSwimLane(lane) {
615
+ this.swimLanesState = [...this.swimLanesState, lane];
616
+ this.draw();
617
+ }
618
+ updateSwimLane(id, updates) {
619
+ this.swimLanesState = this.swimLanesState.map(l => l.id === id ? { ...l, ...updates, id: l.id } : l);
620
+ this.draw();
621
+ }
622
+ removeSwimLane(id) {
623
+ this.swimLanesState = this.swimLanesState.filter(l => l.id !== id);
624
+ this.draw();
625
+ }
626
+ reorderSwimLanes(orderedIds) {
627
+ const byId = new Map(this.swimLanesState.map(l => [l.id, l]));
628
+ const reordered = [];
629
+ for (const id of orderedIds) {
630
+ const lane = byId.get(id);
631
+ if (lane)
632
+ reordered.push(lane);
633
+ }
634
+ this.swimLanesState = reordered;
635
+ this.draw();
636
+ }
637
+ // ── Core draw ──────────────────────────────────────────────────────────
638
+ draw() {
639
+ const canvas = this.canvasRef?.nativeElement;
640
+ if (!canvas)
641
+ return;
642
+ const ctx = canvas.getContext('2d');
643
+ if (!ctx)
644
+ return;
645
+ const rect = canvas.getBoundingClientRect();
646
+ const w = rect.width;
647
+ const h = rect.height;
648
+ if (w === 0 || h === 0)
649
+ return;
650
+ const dpr = window.devicePixelRatio || 1;
651
+ const physW = Math.round(w * dpr);
652
+ const physH = Math.round(h * dpr);
653
+ if (canvas.width !== physW || canvas.height !== physH) {
654
+ canvas.width = physW;
655
+ canvas.height = physH;
656
+ }
657
+ ctx.save();
658
+ ctx.scale(dpr, dpr);
659
+ const clampedScrollTop = drawTimeline(ctx, w, h, {
660
+ startMs: this.startMs,
661
+ endMs: this.endMs,
662
+ currentMs: this.curMs,
663
+ theme: this.theme,
664
+ maxTicks: this.maxTicks,
665
+ swimLanes: this.swimLanesState,
666
+ showSwimLanes: this.showSwimLanesState,
667
+ scrollTop: this.scrollTop,
668
+ reorderState: this.reorderState,
669
+ });
670
+ if (clampedScrollTop !== this.scrollTop) {
671
+ this.scrollTop = clampedScrollTop;
672
+ }
673
+ ctx.restore();
674
+ }
675
+ // ── Hit testing (delegates to core) ────────────────────────────────────
676
+ hitTestSwimLane(x, y, w, h) {
677
+ return hitTestSwimLane(x, y, w, h, {
678
+ startMs: this.startMs, endMs: this.endMs,
679
+ theme: this.theme,
680
+ swimLanes: this.swimLanesState, showSwimLanes: this.showSwimLanesState,
681
+ scrollTop: this.scrollTop,
682
+ });
683
+ }
684
+ hitTestLaneLabel(x, y, h) {
685
+ return hitTestLaneLabel(x, y, h, {
686
+ swimLanes: this.swimLanesState, showSwimLanes: this.showSwimLanesState,
687
+ scrollTop: this.scrollTop,
688
+ });
689
+ }
690
+ isInSwimLaneRegion(y, h) {
691
+ return isInSwimLaneRegion(y, h, {
692
+ swimLanes: this.swimLanesState, showSwimLanes: this.showSwimLanesState,
693
+ });
694
+ }
695
+ // ── Zoom helper ────────────────────────────────────────────────────────
696
+ zoomFrom(amount) {
697
+ const result = zoomRange(this.startMs, this.endMs, amount);
698
+ this.startMs = result.startMs;
699
+ this.endMs = result.endMs;
700
+ this.draw();
701
+ }
702
+ // ── Edge scroll ────────────────────────────────────────────────────────
703
+ startEdgeScroll(direction) {
704
+ if (this.edgeRAF !== null)
705
+ return;
706
+ const scroll = () => {
707
+ const canvas = this.canvasRef?.nativeElement;
708
+ const span = this.endMs - this.startMs;
709
+ const shift = direction * span * 0.01;
710
+ this.startMs += shift;
711
+ this.endMs += shift;
712
+ if (canvas) {
713
+ const rect = canvas.getBoundingClientRect();
714
+ const cx = Math.max(0, Math.min(rect.width, this.scrubClientX - rect.left));
715
+ const ms = this.startMs + (cx / rect.width) * (this.endMs - this.startMs);
716
+ this.curMs = ms;
717
+ this.ngZone.run(() => this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(ms))));
718
+ }
719
+ this.draw();
720
+ this.edgeRAF = requestAnimationFrame(scroll);
721
+ };
722
+ this.edgeRAF = requestAnimationFrame(scroll);
723
+ }
724
+ stopEdgeScroll() {
725
+ if (this.edgeRAF !== null) {
726
+ cancelAnimationFrame(this.edgeRAF);
727
+ this.edgeRAF = null;
728
+ }
729
+ }
730
+ getTouchDist(a, b) {
731
+ return Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY);
732
+ }
733
+ // ── Mouse handlers (template-bound) ────────────────────────────────────
734
+ onCanvasMouseDown(e) {
735
+ e.preventDefault();
736
+ const canvas = this.canvasRef.nativeElement;
737
+ const rect = canvas.getBoundingClientRect();
738
+ const x = e.clientX - rect.left;
739
+ const y = e.clientY - rect.top;
740
+ // Reorder drag on lane label
741
+ if (e.button === 0 && this.swimLaneReorder.observed) {
742
+ const labelLane = this.hitTestLaneLabel(x, y, rect.height);
743
+ if (labelLane) {
744
+ const lanes = this.swimLanesState;
745
+ const dragIdx = lanes.findIndex(l => l.id === labelLane.id);
746
+ this.reorderState = {
747
+ dragging: true,
748
+ dragLaneId: labelLane.id,
749
+ dragStartY: e.clientY,
750
+ currentY: e.clientY,
751
+ insertIndex: dragIdx,
752
+ };
753
+ canvas.style.cursor = 'grabbing';
754
+ return;
755
+ }
756
+ }
757
+ // Swim lane item click detection
758
+ if (e.button === 0 && this.isInSwimLaneRegion(y, rect.height)) {
759
+ const needleX = ((this.curMs - this.startMs) / (this.endMs - this.startMs)) * rect.width;
760
+ const nearNeedle = Math.abs(x - needleX) <= 10;
761
+ if (!nearNeedle) {
762
+ const hit = this.hitTestSwimLane(x, y, rect.width, rect.height);
763
+ if (hit) {
764
+ this.swimLaneDownTime = performance.now();
765
+ return;
766
+ }
767
+ }
768
+ }
769
+ if (e.button === 0) {
770
+ this.mouseMode = 'scrub';
771
+ this.scrubClientX = e.clientX;
772
+ canvas.style.cursor = 'grabbing';
773
+ this.ngZone.run(() => this.dragStart.emit());
774
+ const ms = this.startMs + (x / rect.width) * (this.endMs - this.startMs);
775
+ this.curMs = ms;
776
+ this.draw();
777
+ this.ngZone.run(() => this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(ms))));
778
+ }
779
+ else if (e.button === 1) {
780
+ this.mouseMode = 'slide';
781
+ this.mouseX = e.clientX;
782
+ }
783
+ else if (e.button === 2) {
784
+ if (this.swimLaneItemContextMenu.observed && this.isInSwimLaneRegion(y, rect.height))
785
+ return;
786
+ this.mouseMode = 'zoom';
787
+ this.mouseX = e.clientX;
788
+ }
789
+ }
790
+ onDocMouseMove(e) {
791
+ // Reorder drag
792
+ const rs = this.reorderState;
793
+ if (rs && rs.dragging) {
794
+ rs.currentY = e.clientY;
795
+ const canvas = this.canvasRef?.nativeElement;
796
+ if (canvas) {
797
+ const rect = canvas.getBoundingClientRect();
798
+ const canvasY = e.clientY - rect.top;
799
+ let y = -this.scrollTop;
800
+ const lanes = this.swimLanesState;
801
+ let idx = lanes.length;
802
+ for (let i = 0; i < lanes.length; i++) {
803
+ const laneH = lanes[i].height ?? DEFAULT_LANE_HEIGHT;
804
+ const mid = y + laneH / 2;
805
+ if (canvasY < mid) {
806
+ idx = i;
807
+ break;
808
+ }
809
+ y += laneH + LANE_GAP;
810
+ }
811
+ rs.insertIndex = idx;
812
+ }
813
+ this.draw();
814
+ return;
815
+ }
816
+ if (this.mouseMode === 'none')
817
+ return;
818
+ const canvas = this.canvasRef?.nativeElement;
819
+ if (!canvas)
820
+ return;
821
+ const rect = canvas.getBoundingClientRect();
822
+ const w = rect.width;
823
+ if (this.mouseMode === 'scrub') {
824
+ this.scrubClientX = e.clientX;
825
+ const x = e.clientX - rect.left;
826
+ const edge = w * 0.08;
827
+ if (x < edge)
828
+ this.startEdgeScroll(-1);
829
+ else if (x > w - edge)
830
+ this.startEdgeScroll(1);
831
+ else
832
+ this.stopEdgeScroll();
833
+ const cx = Math.max(0, Math.min(w, x));
834
+ const ms = this.startMs + (cx / w) * (this.endMs - this.startMs);
835
+ this.curMs = ms;
836
+ this.draw();
837
+ this.ngZone.run(() => this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(ms))));
838
+ }
839
+ else if (this.mouseMode === 'slide') {
840
+ const dx = this.mouseX - e.clientX;
841
+ this.mouseX = e.clientX;
842
+ if (dx !== 0) {
843
+ const shift = (dx / w) * (this.endMs - this.startMs);
844
+ this.startMs += shift;
845
+ this.endMs += shift;
846
+ this.draw();
847
+ }
848
+ }
849
+ else if (this.mouseMode === 'zoom') {
850
+ const dx = this.mouseX - e.clientX;
851
+ this.mouseX = e.clientX;
852
+ if (dx !== 0)
853
+ this.zoomFrom(Math.pow(1.01, dx));
854
+ }
855
+ }
856
+ onDocMouseUp() {
857
+ // Finish reorder drag
858
+ const rs = this.reorderState;
859
+ if (rs && rs.dragging) {
860
+ const dragDistance = Math.abs(rs.currentY - rs.dragStartY);
861
+ const lanes = this.swimLanesState;
862
+ const dragIdx = lanes.findIndex(l => l.id === rs.dragLaneId);
863
+ if (dragDistance > 5 && dragIdx >= 0 && rs.insertIndex !== dragIdx && rs.insertIndex !== dragIdx + 1) {
864
+ const newLanes = [...lanes];
865
+ const [removed] = newLanes.splice(dragIdx, 1);
866
+ const insertAt = rs.insertIndex > dragIdx ? rs.insertIndex - 1 : rs.insertIndex;
867
+ newLanes.splice(insertAt, 0, removed);
868
+ this.swimLanesState = newLanes;
869
+ this.ngZone.run(() => this.swimLaneReorder.emit(newLanes.map(l => l.id)));
870
+ }
871
+ this.reorderState = null;
872
+ const canvas = this.canvasRef?.nativeElement;
873
+ if (canvas)
874
+ canvas.style.cursor = 'default';
875
+ this.draw();
876
+ return;
877
+ }
878
+ this.stopEdgeScroll();
879
+ this.mouseMode = 'none';
880
+ const canvas = this.canvasRef?.nativeElement;
881
+ if (canvas)
882
+ canvas.style.cursor = 'default';
883
+ this.ngZone.run(() => this.dragEnd.emit());
884
+ }
885
+ onCanvasMouseMove(e) {
886
+ if (this.mouseMode !== 'none')
887
+ return;
888
+ const canvas = this.canvasRef.nativeElement;
889
+ const rect = canvas.getBoundingClientRect();
890
+ const x = e.clientX - rect.left;
891
+ const y = e.clientY - rect.top;
892
+ const needleX = ((this.curMs - this.startMs) / (this.endMs - this.startMs)) * rect.width;
893
+ const nearNeedle = Math.abs(x - needleX) <= 10;
894
+ if (this.isInSwimLaneRegion(y, rect.height)) {
895
+ const hit = this.hitTestSwimLane(x, y, rect.width, rect.height);
896
+ const prev = this.hoveredItem;
897
+ if (hit) {
898
+ canvas.style.cursor = nearNeedle ? 'grab' : 'pointer';
899
+ if (!prev || prev.item.id !== hit.item.id || prev.lane.id !== hit.lane.id) {
900
+ this.hoveredItem = hit;
901
+ this.ngZone.run(() => this.swimLaneItemHover.emit({ laneId: hit.lane.id, item: hit.item, originalEvent: e }));
902
+ this.draw();
903
+ }
904
+ }
905
+ else {
906
+ if (prev) {
907
+ this.hoveredItem = null;
908
+ this.ngZone.run(() => this.swimLaneItemHover.emit(null));
909
+ this.draw();
910
+ }
911
+ if (nearNeedle) {
912
+ canvas.style.cursor = 'grab';
913
+ }
914
+ else {
915
+ const labelLane = this.hitTestLaneLabel(x, y, rect.height);
916
+ canvas.style.cursor = labelLane && this.swimLaneReorder.observed ? 'grab' : 'default';
917
+ }
918
+ }
919
+ return;
920
+ }
921
+ if (this.hoveredItem) {
922
+ this.hoveredItem = null;
923
+ this.ngZone.run(() => this.swimLaneItemHover.emit(null));
924
+ this.draw();
925
+ }
926
+ canvas.style.cursor = nearNeedle ? 'grab' : 'default';
927
+ }
928
+ onCanvasClick(e) {
929
+ const elapsed = performance.now() - this.swimLaneDownTime;
930
+ if (elapsed > 300)
931
+ return;
932
+ const rect = e.target.getBoundingClientRect();
933
+ const x = e.clientX - rect.left;
934
+ const y = e.clientY - rect.top;
935
+ const hit = this.hitTestSwimLane(x, y, rect.width, rect.height);
936
+ if (hit) {
937
+ this.ngZone.run(() => this.swimLaneItemClick.emit({ laneId: hit.lane.id, item: hit.item, originalEvent: e }));
938
+ }
939
+ }
940
+ onCanvasDblClick(e) {
941
+ const rect = e.target.getBoundingClientRect();
942
+ const x = e.clientX - rect.left;
943
+ const y = e.clientY - rect.top;
944
+ const hit = this.hitTestSwimLane(x, y, rect.width, rect.height);
945
+ if (hit) {
946
+ this.ngZone.run(() => this.swimLaneItemDoubleClick.emit({ laneId: hit.lane.id, item: hit.item, originalEvent: e }));
947
+ }
948
+ }
949
+ onCanvasContextMenu(e) {
950
+ const rect = e.target.getBoundingClientRect();
951
+ const x = e.clientX - rect.left;
952
+ const y = e.clientY - rect.top;
953
+ const hit = this.hitTestSwimLane(x, y, rect.width, rect.height);
954
+ if (hit && this.swimLaneItemContextMenu.observed) {
955
+ e.preventDefault();
956
+ this.ngZone.run(() => this.swimLaneItemContextMenu.emit({ laneId: hit.lane.id, item: hit.item, originalEvent: e }));
957
+ }
958
+ else {
959
+ e.preventDefault();
960
+ }
961
+ }
962
+ onCanvasMouseLeave() {
963
+ if (this.hoveredItem) {
964
+ this.hoveredItem = null;
965
+ this.ngZone.run(() => this.swimLaneItemHover.emit(null));
966
+ this.draw();
967
+ }
968
+ const canvas = this.canvasRef?.nativeElement;
969
+ if (this.mouseMode === 'none' && canvas)
970
+ canvas.style.cursor = 'default';
971
+ }
972
+ // ── Wheel handler ──────────────────────────────────────────────────────
973
+ onWheel(e) {
974
+ e.preventDefault();
975
+ const canvas = this.canvasRef?.nativeElement;
976
+ if (!canvas)
977
+ return;
978
+ const rect = canvas.getBoundingClientRect();
979
+ const y = e.clientY - rect.top;
980
+ if (this.isInSwimLaneRegion(y, rect.height)) {
981
+ const lanes = this.swimLanesState;
982
+ const totalH = totalSwimLaneHeight(lanes);
983
+ const laneRegionH = Math.max(0, rect.height - TICK_AREA_HEIGHT);
984
+ const maxScroll = Math.max(0, totalH - laneRegionH);
985
+ if (maxScroll > 0) {
986
+ this.scrollTop = Math.max(0, Math.min(maxScroll, this.scrollTop + e.deltaY * SWIM_LANE_SCROLL_SPEED));
987
+ this.draw();
988
+ return;
989
+ }
990
+ }
991
+ this.zoomFrom(Math.pow(1.05, e.deltaY > 0 ? -1 : 1));
992
+ }
993
+ // ── Touch handlers ─────────────────────────────────────────────────────
994
+ onTouchStart(e) {
995
+ e.preventDefault();
996
+ const canvas = this.canvasRef.nativeElement;
997
+ const rect = canvas.getBoundingClientRect();
998
+ if (e.touches.length === 1) {
999
+ const x = e.touches[0].clientX - rect.left;
1000
+ const cx = Math.max(0, Math.min(rect.width, x));
1001
+ const ms = this.startMs + (cx / rect.width) * (this.endMs - this.startMs);
1002
+ this.touchMode = 'scrub';
1003
+ this.touchX = e.touches[0].clientX;
1004
+ this.scrubClientX = e.touches[0].clientX;
1005
+ this.curMs = ms;
1006
+ this.draw();
1007
+ this.ngZone.run(() => {
1008
+ this.dragStart.emit();
1009
+ this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(ms)));
1010
+ });
1011
+ }
1012
+ else if (e.touches.length >= 2) {
1013
+ this.touchMode = 'pinch';
1014
+ this.pinchDist = this.getTouchDist(e.touches[0], e.touches[1]);
1015
+ }
1016
+ }
1017
+ onTouchMove(e) {
1018
+ e.preventDefault();
1019
+ const canvas = this.canvasRef.nativeElement;
1020
+ const rect = canvas.getBoundingClientRect();
1021
+ if (this.touchMode === 'scrub' && e.touches.length >= 1) {
1022
+ const x = e.touches[0].clientX - rect.left;
1023
+ const edge = rect.width * 0.08;
1024
+ this.scrubClientX = e.touches[0].clientX;
1025
+ if (x < edge) {
1026
+ this.startEdgeScroll(-1);
1027
+ }
1028
+ else if (x > rect.width - edge) {
1029
+ this.startEdgeScroll(1);
1030
+ }
1031
+ else {
1032
+ this.stopEdgeScroll();
1033
+ const cx = Math.max(0, Math.min(rect.width, x));
1034
+ const ms = this.startMs + (cx / rect.width) * (this.endMs - this.startMs);
1035
+ this.curMs = ms;
1036
+ this.draw();
1037
+ this.ngZone.run(() => this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(ms))));
1038
+ }
1039
+ }
1040
+ else if (this.touchMode === 'slide' && e.touches.length >= 1) {
1041
+ const dx = this.touchX - e.touches[0].clientX;
1042
+ this.touchX = e.touches[0].clientX;
1043
+ if (dx !== 0) {
1044
+ const shift = (dx / rect.width) * (this.endMs - this.startMs);
1045
+ this.startMs += shift;
1046
+ this.endMs += shift;
1047
+ this.draw();
1048
+ }
1049
+ }
1050
+ else if (this.touchMode === 'pinch' && e.touches.length >= 2) {
1051
+ const newDist = this.getTouchDist(e.touches[0], e.touches[1]);
1052
+ if (newDist > 0 && this.pinchDist > 0) {
1053
+ this.zoomFrom(this.pinchDist / newDist);
1054
+ }
1055
+ this.pinchDist = newDist;
1056
+ }
1057
+ }
1058
+ onTouchEnd(e) {
1059
+ this.stopEdgeScroll();
1060
+ if (this.touchMode === 'scrub') {
1061
+ this.ngZone.run(() => this.dragEnd.emit());
1062
+ }
1063
+ if (e.touches.length === 0) {
1064
+ this.touchMode = 'none';
1065
+ }
1066
+ else if (e.touches.length === 1) {
1067
+ this.touchMode = 'slide';
1068
+ this.touchX = e.touches[0].clientX;
1069
+ }
1070
+ }
1071
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: TimelineCanvasComponent, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
1072
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.10", type: TimelineCanvasComponent, isStandalone: true, selector: "ct-timeline-canvas", inputs: { currentTime: "currentTime", defaultStartMs: "defaultStartMs", defaultEndMs: "defaultEndMs", theme: "theme", maxTicks: "maxTicks", swimLanes: "swimLanes", showSwimLanes: "showSwimLanes" }, outputs: { timeChange: "timeChange", dragStart: "dragStart", dragEnd: "dragEnd", swimLaneItemClick: "swimLaneItemClick", swimLaneItemHover: "swimLaneItemHover", swimLaneItemDoubleClick: "swimLaneItemDoubleClick", swimLaneItemContextMenu: "swimLaneItemContextMenu", swimLaneReorder: "swimLaneReorder" }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["canvas"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `<canvas #canvas
1073
+ style="width:100%;flex:1;min-height:0;display:block;cursor:default"
1074
+ (mousedown)="onCanvasMouseDown($event)"
1075
+ (mousemove)="onCanvasMouseMove($event)"
1076
+ (click)="onCanvasClick($event)"
1077
+ (dblclick)="onCanvasDblClick($event)"
1078
+ (contextmenu)="onCanvasContextMenu($event)"
1079
+ (mouseleave)="onCanvasMouseLeave()"
1080
+ ></canvas>`, isInline: true, styles: [":host{display:flex;flex:1;min-height:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1081
+ }
1082
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: TimelineCanvasComponent, decorators: [{
1083
+ type: Component,
1084
+ args: [{ selector: 'ct-timeline-canvas', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `<canvas #canvas
1085
+ style="width:100%;flex:1;min-height:0;display:block;cursor:default"
1086
+ (mousedown)="onCanvasMouseDown($event)"
1087
+ (mousemove)="onCanvasMouseMove($event)"
1088
+ (click)="onCanvasClick($event)"
1089
+ (dblclick)="onCanvasDblClick($event)"
1090
+ (contextmenu)="onCanvasContextMenu($event)"
1091
+ (mouseleave)="onCanvasMouseLeave()"
1092
+ ></canvas>`, styles: [":host{display:flex;flex:1;min-height:0}\n"] }]
1093
+ }], ctorParameters: () => [{ type: i0.NgZone }], propDecorators: { currentTime: [{
1094
+ type: Input
1095
+ }], defaultStartMs: [{
1096
+ type: Input
1097
+ }], defaultEndMs: [{
1098
+ type: Input
1099
+ }], theme: [{
1100
+ type: Input
1101
+ }], maxTicks: [{
1102
+ type: Input
1103
+ }], swimLanes: [{
1104
+ type: Input
1105
+ }], showSwimLanes: [{
1106
+ type: Input
1107
+ }], timeChange: [{
1108
+ type: Output
1109
+ }], dragStart: [{
1110
+ type: Output
1111
+ }], dragEnd: [{
1112
+ type: Output
1113
+ }], swimLaneItemClick: [{
1114
+ type: Output
1115
+ }], swimLaneItemHover: [{
1116
+ type: Output
1117
+ }], swimLaneItemDoubleClick: [{
1118
+ type: Output
1119
+ }], swimLaneItemContextMenu: [{
1120
+ type: Output
1121
+ }], swimLaneReorder: [{
1122
+ type: Output
1123
+ }], canvasRef: [{
1124
+ type: ViewChild,
1125
+ args: ['canvas']
1126
+ }] } });
1127
+
1128
+ const DEFAULT_FF_SPEEDS = [2, 4, 8, 16, 32, 100, 1];
1129
+ const DEFAULT_RW_SPEEDS = [1, 2, 4, 8, 16, 32, 100];
1130
+ class TimelineComponent {
1131
+ cdr;
1132
+ ngZone;
1133
+ // ── Inputs ─────────────────────────────────────────────────────────────
1134
+ startTime;
1135
+ endTime;
1136
+ currentTime;
1137
+ clock;
1138
+ height;
1139
+ showControls = true;
1140
+ enableDrag = true;
1141
+ dateTimeFormat;
1142
+ jumpToTime;
1143
+ maxTicks;
1144
+ ffSpeeds = DEFAULT_FF_SPEEDS;
1145
+ rwSpeeds = DEFAULT_RW_SPEEDS;
1146
+ theme;
1147
+ cssClass;
1148
+ swimLanes;
1149
+ showSwimLanes;
1150
+ swimLaneTransition = 'animated';
1151
+ // ── Outputs ────────────────────────────────────────────────────────────
1152
+ timeChange = new EventEmitter();
1153
+ playPause = new EventEmitter();
1154
+ multiplierChange = new EventEmitter();
1155
+ dateTimeClick = new EventEmitter();
1156
+ showSwimLanesChange = new EventEmitter();
1157
+ swimLaneItemClick = new EventEmitter();
1158
+ swimLaneItemHover = new EventEmitter();
1159
+ swimLaneItemDoubleClick = new EventEmitter();
1160
+ swimLaneItemContextMenu = new EventEmitter();
1161
+ swimLaneReorder = new EventEmitter();
1162
+ // ── ViewChild refs ─────────────────────────────────────────────────────
1163
+ canvasComp;
1164
+ controlsRef;
1165
+ // ── Internal state ─────────────────────────────────────────────────────
1166
+ currentTimeState;
1167
+ isPlayingState = false;
1168
+ multiplierState = 1;
1169
+ swimLanesExpanded = true;
1170
+ controlsHeight = 0;
1171
+ isDragging = false;
1172
+ defaultStartMs = 0;
1173
+ defaultEndMs = 0;
1174
+ finalTheme = { ...defaultTheme };
1175
+ ro;
1176
+ clockCleanup;
1177
+ fallbackInterval;
1178
+ constructor(cdr, ngZone) {
1179
+ this.cdr = cdr;
1180
+ this.ngZone = ngZone;
1181
+ }
1182
+ get hasSwimLanes() {
1183
+ return this.swimLanes != null && this.swimLanes.length > 0;
1184
+ }
1185
+ get isLive() {
1186
+ return Math.abs(Cesium.JulianDate.toDate(this.currentTimeState).getTime() - Date.now()) < 10_000;
1187
+ }
1188
+ get isCollapsed() {
1189
+ return this.hasSwimLanes && !this.swimLanesExpanded;
1190
+ }
1191
+ get heightStyle() {
1192
+ if (this.isCollapsed)
1193
+ return `${this.controlsHeight + TICK_AREA_HEIGHT}px`;
1194
+ if (this.height != null)
1195
+ return `${this.height}px`;
1196
+ return '100%';
1197
+ }
1198
+ // ── Lifecycle ──────────────────────────────────────────────────────────
1199
+ ngOnInit() {
1200
+ const now = Date.now();
1201
+ this.defaultStartMs = this.startTime
1202
+ ? Cesium.JulianDate.toDate(toJulianDate(this.startTime)).getTime()
1203
+ : now - 12 * 3600 * 1000;
1204
+ this.defaultEndMs = this.endTime
1205
+ ? Cesium.JulianDate.toDate(toJulianDate(this.endTime)).getTime()
1206
+ : now + 12 * 3600 * 1000;
1207
+ this.currentTimeState = toJulianDate(this.currentTime ?? (this.startTime ?? Cesium.JulianDate.fromDate(new Date())));
1208
+ this.isPlayingState = this.clock?.shouldAnimate ?? false;
1209
+ this.multiplierState = this.clock?.multiplier ?? 1;
1210
+ this.swimLanesExpanded = this.showSwimLanes ?? true;
1211
+ this.finalTheme = { ...defaultTheme, ...this.theme };
1212
+ }
1213
+ ngAfterViewInit() {
1214
+ const el = this.controlsRef?.nativeElement;
1215
+ if (el) {
1216
+ this.ro = new ResizeObserver(([entry]) => {
1217
+ this.controlsHeight = entry.borderBoxSize[0].blockSize;
1218
+ this.cdr.markForCheck();
1219
+ });
1220
+ this.ro.observe(el);
1221
+ }
1222
+ // Clock sync
1223
+ this.setupClockSync();
1224
+ this.cdr.detectChanges();
1225
+ }
1226
+ ngOnChanges(changes) {
1227
+ if (changes['theme']) {
1228
+ this.finalTheme = { ...defaultTheme, ...this.theme };
1229
+ }
1230
+ if (changes['showSwimLanes'] && this.showSwimLanes != null) {
1231
+ this.swimLanesExpanded = this.showSwimLanes;
1232
+ }
1233
+ if (changes['clock'] && !changes['clock'].firstChange) {
1234
+ this.cleanupClockSync();
1235
+ this.setupClockSync();
1236
+ }
1237
+ if (changes['jumpToTime'] && this.jumpToTime) {
1238
+ const t = toJulianDate(this.jumpToTime);
1239
+ this.handleTimeChange(t);
1240
+ if (this.canvasComp) {
1241
+ const { startMs, endMs } = this.canvasComp.getVisibleRange();
1242
+ const span = endMs - startMs;
1243
+ const newMs = Cesium.JulianDate.toDate(t).getTime();
1244
+ this.canvasComp.zoomTo(newMs - span / 2, newMs + span / 2);
1245
+ }
1246
+ }
1247
+ }
1248
+ ngOnDestroy() {
1249
+ this.cleanupClockSync();
1250
+ this.ro?.disconnect();
1251
+ if (this.fallbackInterval)
1252
+ clearInterval(this.fallbackInterval);
1253
+ }
1254
+ // ── Clock sync ─────────────────────────────────────────────────────────
1255
+ setupClockSync() {
1256
+ if (this.clock) {
1257
+ const onTick = () => {
1258
+ if (this.isDragging)
1259
+ return;
1260
+ const ct = Cesium.JulianDate.clone(this.clock.currentTime);
1261
+ this.currentTimeState = ct;
1262
+ this.isPlayingState = this.clock.shouldAnimate;
1263
+ this.multiplierState = this.clock.multiplier;
1264
+ if (this.canvasComp) {
1265
+ const { startMs, endMs } = this.canvasComp.getVisibleRange();
1266
+ const span = endMs - startMs;
1267
+ const ctMs = Cesium.JulianDate.toDate(ct).getTime();
1268
+ const pos = ctMs - startMs;
1269
+ if (pos <= span * 0.1) {
1270
+ this.canvasComp.zoomTo(ctMs - span * 0.1, ctMs + span * 0.9, ctMs);
1271
+ }
1272
+ else if (pos >= span * 0.9) {
1273
+ this.canvasComp.zoomTo(ctMs - span * 0.9, ctMs + span * 0.1, ctMs);
1274
+ }
1275
+ }
1276
+ this.cdr.markForCheck();
1277
+ };
1278
+ this.clock.onTick.addEventListener(onTick);
1279
+ this.clockCleanup = () => this.clock.onTick.removeEventListener(onTick);
1280
+ }
1281
+ else {
1282
+ this.ngZone.runOutsideAngular(() => {
1283
+ this.fallbackInterval = setInterval(() => {
1284
+ if (this.isDragging)
1285
+ return;
1286
+ const ct = Cesium.JulianDate.fromDate(new Date());
1287
+ this.currentTimeState = ct;
1288
+ if (this.canvasComp) {
1289
+ const { startMs, endMs } = this.canvasComp.getVisibleRange();
1290
+ const span = endMs - startMs;
1291
+ const ctMs = Cesium.JulianDate.toDate(ct).getTime();
1292
+ const pos = ctMs - startMs;
1293
+ if (pos <= span * 0.1)
1294
+ this.canvasComp.zoomTo(ctMs - span * 0.1, ctMs + span * 0.9, ctMs);
1295
+ else if (pos >= span * 0.9)
1296
+ this.canvasComp.zoomTo(ctMs - span * 0.9, ctMs + span * 0.1, ctMs);
1297
+ }
1298
+ this.ngZone.run(() => this.cdr.markForCheck());
1299
+ }, 1000);
1300
+ });
1301
+ }
1302
+ }
1303
+ cleanupClockSync() {
1304
+ this.clockCleanup?.();
1305
+ this.clockCleanup = undefined;
1306
+ if (this.fallbackInterval) {
1307
+ clearInterval(this.fallbackInterval);
1308
+ this.fallbackInterval = undefined;
1309
+ }
1310
+ }
1311
+ // ── Handlers ───────────────────────────────────────────────────────────
1312
+ handleTimeChange(t) {
1313
+ this.currentTimeState = t;
1314
+ if (this.clock)
1315
+ this.clock.currentTime = Cesium.JulianDate.clone(t);
1316
+ this.timeChange.emit(t);
1317
+ this.cdr.markForCheck();
1318
+ }
1319
+ handlePlayPause(playing) {
1320
+ if (playing && this.multiplierState < 0) {
1321
+ this.applyMultiplier(1, false);
1322
+ }
1323
+ if (this.clock)
1324
+ this.clock.shouldAnimate = playing;
1325
+ this.isPlayingState = playing;
1326
+ this.playPause.emit(playing);
1327
+ this.cdr.markForCheck();
1328
+ }
1329
+ handleFastForward() {
1330
+ const speeds = this.ffSpeeds.length > 0 ? this.ffSpeeds : DEFAULT_FF_SPEEDS;
1331
+ const cur = this.multiplierState > 1 ? this.multiplierState : 1;
1332
+ const idx = speeds.indexOf(cur);
1333
+ const next = speeds[idx < 0 || idx === speeds.length - 1 ? 0 : idx + 1];
1334
+ this.applyMultiplier(next);
1335
+ }
1336
+ handleRewindSpeed() {
1337
+ const speeds = this.rwSpeeds.length > 0 ? this.rwSpeeds : DEFAULT_RW_SPEEDS;
1338
+ const curAbs = this.multiplierState < 0 ? Math.abs(this.multiplierState) : 0;
1339
+ const idx = speeds.indexOf(curAbs);
1340
+ const next = -(speeds[idx < 0 || idx === speeds.length - 1 ? 0 : idx + 1]);
1341
+ this.applyMultiplier(next);
1342
+ }
1343
+ handleJumpToStart() {
1344
+ const t = toJulianDate(this.startTime ?? Cesium.JulianDate.fromDate(new Date(this.defaultStartMs)));
1345
+ if (this.clock)
1346
+ this.clock.currentTime = Cesium.JulianDate.clone(t);
1347
+ this.currentTimeState = t;
1348
+ this.canvasComp?.zoomTo(this.defaultStartMs, this.defaultEndMs);
1349
+ this.cdr.markForCheck();
1350
+ }
1351
+ handleJumpToEnd() {
1352
+ const t = toJulianDate(this.endTime ?? Cesium.JulianDate.fromDate(new Date(this.defaultEndMs)));
1353
+ if (this.clock)
1354
+ this.clock.currentTime = Cesium.JulianDate.clone(t);
1355
+ this.currentTimeState = t;
1356
+ this.canvasComp?.zoomTo(this.defaultStartMs, this.defaultEndMs);
1357
+ this.cdr.markForCheck();
1358
+ }
1359
+ handleJumpToLive() {
1360
+ const t = Cesium.JulianDate.fromDate(new Date());
1361
+ if (this.clock)
1362
+ this.clock.currentTime = Cesium.JulianDate.clone(t);
1363
+ this.currentTimeState = t;
1364
+ this.applyMultiplier(1);
1365
+ const nowMs = Date.now();
1366
+ this.canvasComp?.zoomTo(nowMs - 12 * 3600 * 1000, nowMs + 12 * 3600 * 1000);
1367
+ this.cdr.markForCheck();
1368
+ }
1369
+ handleToggleSwimLanes() {
1370
+ this.swimLanesExpanded = !this.swimLanesExpanded;
1371
+ this.showSwimLanesChange.emit(this.swimLanesExpanded);
1372
+ this.cdr.markForCheck();
1373
+ }
1374
+ applyMultiplier(m, play = true) {
1375
+ if (this.clock) {
1376
+ this.clock.multiplier = m;
1377
+ if (play)
1378
+ this.clock.shouldAnimate = true;
1379
+ }
1380
+ this.multiplierState = m;
1381
+ if (play)
1382
+ this.isPlayingState = true;
1383
+ this.multiplierChange.emit(m);
1384
+ this.cdr.markForCheck();
1385
+ }
1386
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: TimelineComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
1387
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: TimelineComponent, isStandalone: true, selector: "ct-timeline", inputs: { startTime: "startTime", endTime: "endTime", currentTime: "currentTime", clock: "clock", height: "height", showControls: "showControls", enableDrag: "enableDrag", dateTimeFormat: "dateTimeFormat", jumpToTime: "jumpToTime", maxTicks: "maxTicks", ffSpeeds: "ffSpeeds", rwSpeeds: "rwSpeeds", theme: "theme", cssClass: "cssClass", swimLanes: "swimLanes", showSwimLanes: "showSwimLanes", swimLaneTransition: "swimLaneTransition" }, outputs: { timeChange: "timeChange", playPause: "playPause", multiplierChange: "multiplierChange", dateTimeClick: "dateTimeClick", showSwimLanesChange: "showSwimLanesChange", swimLaneItemClick: "swimLaneItemClick", swimLaneItemHover: "swimLaneItemHover", swimLaneItemDoubleClick: "swimLaneItemDoubleClick", swimLaneItemContextMenu: "swimLaneItemContextMenu", swimLaneReorder: "swimLaneReorder" }, viewQueries: [{ propertyName: "canvasComp", first: true, predicate: TimelineCanvasComponent, descendants: true }, { propertyName: "controlsRef", first: true, predicate: ["controlsEl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
1388
+ <div
1389
+ [class]="cssClass"
1390
+ [style.width]="'100%'"
1391
+ [style.height]="heightStyle"
1392
+ [style.overflow]="'hidden'"
1393
+ [style.display]="'flex'"
1394
+ [style.flex-direction]="'column'"
1395
+ [style.font-family]="'system-ui, -apple-system, sans-serif'"
1396
+ [style.transition]="swimLaneTransition === 'animated' ? 'height 0.2s ease' : undefined"
1397
+ >
1398
+ @if (showControls) {
1399
+ <div #controlsEl>
1400
+ <ct-timeline-controls
1401
+ [currentTime]="currentTimeState"
1402
+ [isPlaying]="isPlayingState"
1403
+ [multiplier]="multiplierState"
1404
+ [isLive]="isLive"
1405
+ [hasStartTime]="startTime != null"
1406
+ [hasEndTime]="endTime != null"
1407
+ [dateTimeFormat]="dateTimeFormat"
1408
+ [theme]="finalTheme"
1409
+ [swimLanesVisible]="hasSwimLanes ? swimLanesExpanded : undefined"
1410
+ (playPause)="handlePlayPause($event)"
1411
+ (jumpToStart)="handleJumpToStart()"
1412
+ (rewind)="handleRewindSpeed()"
1413
+ (fastForward)="handleFastForward()"
1414
+ (jumpToEnd)="handleJumpToEnd()"
1415
+ (jumpToLive)="handleJumpToLive()"
1416
+ (resetSpeed)="applyMultiplier(1)"
1417
+ (dateTimeClick)="dateTimeClick.emit()"
1418
+ (toggleSwimLanes)="handleToggleSwimLanes()"
1419
+ />
1420
+ </div>
1421
+ }
1422
+
1423
+ @if (enableDrag !== false) {
1424
+ <ct-timeline-canvas
1425
+ [currentTime]="currentTimeState"
1426
+ [defaultStartMs]="defaultStartMs"
1427
+ [defaultEndMs]="defaultEndMs"
1428
+ [theme]="finalTheme"
1429
+ [maxTicks]="maxTicks"
1430
+ [swimLanes]="swimLanes"
1431
+ [showSwimLanes]="swimLanesExpanded"
1432
+ (timeChange)="handleTimeChange($event)"
1433
+ (dragStart)="isDragging = true"
1434
+ (dragEnd)="isDragging = false"
1435
+ (swimLaneItemClick)="swimLaneItemClick.emit($event)"
1436
+ (swimLaneItemHover)="swimLaneItemHover.emit($event)"
1437
+ (swimLaneItemDoubleClick)="swimLaneItemDoubleClick.emit($event)"
1438
+ (swimLaneItemContextMenu)="swimLaneItemContextMenu.emit($event)"
1439
+ (swimLaneReorder)="swimLaneReorder.emit($event)"
1440
+ />
1441
+ }
1442
+ </div>
1443
+ `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "component", type: TimelineControlsComponent, selector: "ct-timeline-controls", inputs: ["currentTime", "isPlaying", "multiplier", "dateTimeFormat", "isLive", "hasStartTime", "hasEndTime", "theme", "swimLanesVisible"], outputs: ["dateTimeClick", "playPause", "jumpToStart", "rewind", "fastForward", "jumpToEnd", "jumpToLive", "resetSpeed", "toggleSwimLanes"] }, { kind: "component", type: TimelineCanvasComponent, selector: "ct-timeline-canvas", inputs: ["currentTime", "defaultStartMs", "defaultEndMs", "theme", "maxTicks", "swimLanes", "showSwimLanes"], outputs: ["timeChange", "dragStart", "dragEnd", "swimLaneItemClick", "swimLaneItemHover", "swimLaneItemDoubleClick", "swimLaneItemContextMenu", "swimLaneReorder"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1444
+ }
1445
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: TimelineComponent, decorators: [{
1446
+ type: Component,
1447
+ args: [{ selector: 'ct-timeline', standalone: true, imports: [TimelineControlsComponent, TimelineCanvasComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1448
+ <div
1449
+ [class]="cssClass"
1450
+ [style.width]="'100%'"
1451
+ [style.height]="heightStyle"
1452
+ [style.overflow]="'hidden'"
1453
+ [style.display]="'flex'"
1454
+ [style.flex-direction]="'column'"
1455
+ [style.font-family]="'system-ui, -apple-system, sans-serif'"
1456
+ [style.transition]="swimLaneTransition === 'animated' ? 'height 0.2s ease' : undefined"
1457
+ >
1458
+ @if (showControls) {
1459
+ <div #controlsEl>
1460
+ <ct-timeline-controls
1461
+ [currentTime]="currentTimeState"
1462
+ [isPlaying]="isPlayingState"
1463
+ [multiplier]="multiplierState"
1464
+ [isLive]="isLive"
1465
+ [hasStartTime]="startTime != null"
1466
+ [hasEndTime]="endTime != null"
1467
+ [dateTimeFormat]="dateTimeFormat"
1468
+ [theme]="finalTheme"
1469
+ [swimLanesVisible]="hasSwimLanes ? swimLanesExpanded : undefined"
1470
+ (playPause)="handlePlayPause($event)"
1471
+ (jumpToStart)="handleJumpToStart()"
1472
+ (rewind)="handleRewindSpeed()"
1473
+ (fastForward)="handleFastForward()"
1474
+ (jumpToEnd)="handleJumpToEnd()"
1475
+ (jumpToLive)="handleJumpToLive()"
1476
+ (resetSpeed)="applyMultiplier(1)"
1477
+ (dateTimeClick)="dateTimeClick.emit()"
1478
+ (toggleSwimLanes)="handleToggleSwimLanes()"
1479
+ />
1480
+ </div>
1481
+ }
1482
+
1483
+ @if (enableDrag !== false) {
1484
+ <ct-timeline-canvas
1485
+ [currentTime]="currentTimeState"
1486
+ [defaultStartMs]="defaultStartMs"
1487
+ [defaultEndMs]="defaultEndMs"
1488
+ [theme]="finalTheme"
1489
+ [maxTicks]="maxTicks"
1490
+ [swimLanes]="swimLanes"
1491
+ [showSwimLanes]="swimLanesExpanded"
1492
+ (timeChange)="handleTimeChange($event)"
1493
+ (dragStart)="isDragging = true"
1494
+ (dragEnd)="isDragging = false"
1495
+ (swimLaneItemClick)="swimLaneItemClick.emit($event)"
1496
+ (swimLaneItemHover)="swimLaneItemHover.emit($event)"
1497
+ (swimLaneItemDoubleClick)="swimLaneItemDoubleClick.emit($event)"
1498
+ (swimLaneItemContextMenu)="swimLaneItemContextMenu.emit($event)"
1499
+ (swimLaneReorder)="swimLaneReorder.emit($event)"
1500
+ />
1501
+ }
1502
+ </div>
1503
+ `, styles: [":host{display:block}\n"] }]
1504
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i0.NgZone }], propDecorators: { startTime: [{
1505
+ type: Input
1506
+ }], endTime: [{
1507
+ type: Input
1508
+ }], currentTime: [{
1509
+ type: Input
1510
+ }], clock: [{
1511
+ type: Input
1512
+ }], height: [{
1513
+ type: Input
1514
+ }], showControls: [{
1515
+ type: Input
1516
+ }], enableDrag: [{
1517
+ type: Input
1518
+ }], dateTimeFormat: [{
1519
+ type: Input
1520
+ }], jumpToTime: [{
1521
+ type: Input
1522
+ }], maxTicks: [{
1523
+ type: Input
1524
+ }], ffSpeeds: [{
1525
+ type: Input
1526
+ }], rwSpeeds: [{
1527
+ type: Input
1528
+ }], theme: [{
1529
+ type: Input
1530
+ }], cssClass: [{
1531
+ type: Input
1532
+ }], swimLanes: [{
1533
+ type: Input
1534
+ }], showSwimLanes: [{
1535
+ type: Input
1536
+ }], swimLaneTransition: [{
1537
+ type: Input
1538
+ }], timeChange: [{
1539
+ type: Output
1540
+ }], playPause: [{
1541
+ type: Output
1542
+ }], multiplierChange: [{
1543
+ type: Output
1544
+ }], dateTimeClick: [{
1545
+ type: Output
1546
+ }], showSwimLanesChange: [{
1547
+ type: Output
1548
+ }], swimLaneItemClick: [{
1549
+ type: Output
1550
+ }], swimLaneItemHover: [{
1551
+ type: Output
1552
+ }], swimLaneItemDoubleClick: [{
1553
+ type: Output
1554
+ }], swimLaneItemContextMenu: [{
1555
+ type: Output
1556
+ }], swimLaneReorder: [{
1557
+ type: Output
1558
+ }], canvasComp: [{
1559
+ type: ViewChild,
1560
+ args: [TimelineCanvasComponent]
1561
+ }], controlsRef: [{
1562
+ type: ViewChild,
1563
+ args: ['controlsEl']
1564
+ }] } });
1565
+
1566
+ // Components
1567
+
1568
+ /**
1569
+ * Generated bundle index. Do not edit.
1570
+ */
1571
+
1572
+ export { TimelineCanvasComponent, TimelineComponent, TimelineControlsComponent };
1573
+ //# sourceMappingURL=kteneyck-cesium-timeline-angular.mjs.map