control/timeline/TimelineControl.tstypescript
import { Dialog } from "../../ui/Dialog";
import { ToggleButton } from "../../ui/ToggleButton";
import { Control, type IControlParams } from "../Control";
import { TimelineView } from "./TimelineView";
import type { ITimelineSpan, ITimelineSpanParams, TimelineModel } from "./TimelineModel";
import { createEvents, type EventsHandler } from "../../Events";
type TimelineControlEventsList = [
"visibility",
"change",
"setcurrent",
"current",
"play",
"playback",
"pause",
"reset",
"startdrag",
"stopdrag",
"startdragcurrent",
"stopdragcurrent",
"changelocaltime",
"changelocaldatetime"
];
const TIMELINECONTROL_EVENTS: TimelineControlEventsList = [
"visibility",
"change",
"setcurrent",
"current",
"play",
"playback",
"pause",
"reset",
"startdrag",
"stopdrag",
"startdragcurrent",
"stopdragcurrent",
"changelocaltime",
"changelocaldatetime"
];
interface ITimelineControlParams extends IControlParams {
name?: string;
current?: Date;
rangeStart?: Date;
rangeEnd?: Date;
use24HourClock?: boolean;
template?: string;
}
function addHours(date: Date, hours: number): Date {
const temp = new Date(date);
temp.setHours(temp.getHours() + hours);
return temp;
}
const ICON_BUTTON_SVG = `<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><path d="M500,10C229.4,10,10,229.4,10,500s219.4,490,490,490s490-219.4,490-490S770.6,10,500,10z M800.3,800.3c-39,39-84.5,69.7-135,91C613,913.5,557.4,924.7,500,924.7s-112.9-11.2-165.3-33.3c-50.5-21.3-95.9-52-135-91c-39-39-69.7-84.5-91-135C86.5,612.9,75.3,557.4,75.3,500s11.2-112.9,33.3-165.3c21.3-50.5,52-95.9,91-135c39-39,84.5-69.7,135-91C387.1,86.5,442.6,75.3,500,75.3s112.9,11.2,165.3,33.3c50.5,21.3,95.9,52,135,91c39,39,69.7,84.5,91,135c22.1,52.3,33.3,107.9,33.3,165.3s-11.2,112.9-33.3,165.3C869.9,715.8,839.3,761.2,800.3,800.3z"/><path d="M761.3,532.7H532.7V304c0-18.1-14.6-32.7-32.7-32.7s-32.7,14.6-32.7,32.7v261.3l0,0c0,18.1,14.6,32.7,32.7,32.7h261.3c18.1,0,32.7-14.6,32.7-32.7l0,0C794,547.3,779.4,532.7,761.3,532.7z"/></g>
</svg>`;
class TimelineControl extends Control {
protected _timelineView: TimelineView;
protected _toggleBtn: ToggleButton;
protected _dialog: Dialog<null>;
protected _defaultClockWasRunningBeforeDrag: boolean;
public events: EventsHandler<TimelineControlEventsList>;
constructor(options: ITimelineControlParams = {}) {
super({
name: "timeline",
...options
});
this.events = createEvents(TIMELINECONTROL_EVENTS);
this._defaultClockWasRunningBeforeDrag = false;
let currentDate = options.current || new Date();
let startDate = options.rangeStart || addHours(currentDate, -12);
let endDate = options.rangeEnd || addHours(currentDate, 12);
this._timelineView = new TimelineView({
rangeStart: startDate,
rangeEnd: endDate,
currentDate: currentDate,
use24HourClock: options.use24HourClock,
dateTemplate: options.template
});
this._toggleBtn = new ToggleButton({
classList: ["og-map-button", "og-timeline_button"],
icon: ICON_BUTTON_SVG,
isActive: true
});
this._dialog = new Dialog({
title: "Timeline",
dock: "bottom",
visible: true,
resizable: true,
useHide: true,
top: 10,
left: 60,
width: 600,
height: 128,
minHeight: 128,
maxHeight: 128
});
this._dialog.events.on("visibility", (v: boolean) => {
this._toggleBtn.setActive(v);
this.events.dispatch(this.events.visibility, v);
});
}
/**
* Returns timeline data model.
* @public
*/
public get model(): TimelineModel {
return this._timelineView.model;
}
/** Date the Sun marker stands on. Setting it moves the marker without dispatching. */
public get localDateTime(): Date {
return this._timelineView.localDateTime;
}
public set localDateTime(date: Date) {
this._timelineView.localDateTime = date;
}
/**
* Marker label date format, e.g. "MM/dd/yyyy" or "hh:mm:ss.ms". Tokens: yyyy/yy, MMM,
* MM/M, dd/d, hh/h, mm/m, ss/s, ms, a/A; case matters for M/m only. An empty template
* keeps the adaptive scale format. am/pm follows {@link TimelineControl.use24HourClock}.
*/
public get template(): string {
return this._timelineView.dateTemplate;
}
public set template(template: string) {
this._timelineView.dateTemplate = template;
}
/** Scale time notation: 24-hour, or 12-hour with am/pm. */
public get use24HourClock(): boolean {
return this._timelineView.use24HourClock;
}
public set use24HourClock(use24HourClock: boolean) {
this._timelineView.use24HourClock = use24HourClock;
}
/** Lights the scene by the Sun marker, the civil clock time under the camera. */
public applyLocalDateTime() {
const sun = this.planet?.sun;
if (!sun) return;
sun.setLocalDateTime(this._timelineView.localDateTime);
this.renderer && this.renderer.requestRedraw();
}
/**
* Adds a colored time interval drawn on the scale, e.g. one per telemetry track.
* Spans that overlap in time are placed on separate rows automatically, and the
* rows share the scale height between them.
* @public
* @param {ITimelineSpanParams} params - Span start, end and color. An omitted id is generated.
* @returns {ITimelineSpan} - Stored span, with its id and assigned row.
*/
public addSpan(params: ITimelineSpanParams): ITimelineSpan {
return this.model.addSpan(params);
}
/**
* Adds several spans at once, keeping the ones already on the scale.
* @public
* @param {ITimelineSpanParams[]} params - Spans to add.
* @returns {ITimelineSpan[]} - Stored spans, in the order they were given.
*/
public addSpans(params: ITimelineSpanParams[]): ITimelineSpan[] {
return this.model.addSpans(params);
}
/**
* Replaces every span on the scale, e.g. to redraw the whole set after a source
* was added or removed.
* @public
* @param {ITimelineSpanParams[]} params - Spans the scale is left with.
* @returns {ITimelineSpan[]} - Stored spans, in the order they were given.
*/
public setSpans(params: ITimelineSpanParams[]): ITimelineSpan[] {
return this.model.setSpans(params);
}
/**
* Replaces the time range, color and payload of a span, and reassigns the rows when
* its time range moved.
* @public
* @param {string} id - Span id.
* @param {ITimelineSpanParams} params - New span fields. Every field is replaced, the id is kept.
* @returns {ITimelineSpan | undefined} - Updated span, or undefined when the id is unknown.
*/
public updateSpan(id: string, params: ITimelineSpanParams): ITimelineSpan | undefined {
return this.model.updateSpan(id, params);
}
/**
* Removes a single span from the scale. An unknown id is ignored.
* @public
* @param {string} id - Span id.
*/
public removeSpan(id: string): void {
this.model.removeSpan(id);
}
/**
* Removes every span from the scale.
* @public
*/
public clearSpans(): void {
this.model.clearSpans();
}
/**
* Returns the spans currently on the scale.
* @public
* @returns {ITimelineSpan[]} - Stored spans, each carrying its assigned row.
*/
public getSpans(): ITimelineSpan[] {
return this.model.getSpans();
}
/**
* Returns a single span by its id.
* @public
* @param {string} id - Span id.
* @returns {ITimelineSpan | undefined} - Stored span, or undefined when the id is unknown.
*/
public getSpan(id: string): ITimelineSpan | undefined {
return this.model.getSpan(id);
}
public override oninit() {
let $container = this.renderer!.getUIContainer();
const defaultClock = this.renderer!.handler.defaultClock;
this._toggleBtn.appendTo(this.renderer!.topLeftContainer());
this._dialog.appendTo($container);
this._dialog.events.on("visibility", (v: boolean) => {
if (v) {
this._dialog.positionNearElementOnFirstOpen(this._toggleBtn.el, this.renderer!.getUIContainer());
}
});
this._toggleBtn.events.on("change", (isActive: boolean) => {
this._dialog.setVisibility(isActive);
if (isActive) {
this._timelineView.resize();
}
});
this._timelineView.appendTo(this._dialog.container!);
let sun = this.planet?.sun;
let initialDateTime: Date | null = null;
if (sun) {
if (sun.dateTime) {
initialDateTime = sun.dateTime;
} else if (sun.localDateTime) {
initialDateTime = sun.localDateTime;
}
}
if (sun && initialDateTime) {
let halfRange = this._timelineView.model.range * 0.5;
this._timelineView.model.set(
new Date(initialDateTime.getTime() - halfRange),
new Date(initialDateTime.getTime() + halfRange)
);
this._timelineView.model.current = initialDateTime;
if (!sun.dateTime) {
this._timelineView.localDateTime = initialDateTime;
this._timelineView.localTime = true;
}
}
defaultClock.multiplier = this._timelineView.model.multiplier;
defaultClock.setDate(this._timelineView.model.current);
if (this._timelineView.model.stopped()) {
defaultClock.stop();
} else {
defaultClock.start();
}
this._timelineView.events.on("setcurrent", (d: Date) => {
this.renderer && defaultClock.setDate(d);
if (!this._timelineView.localTime) {
this.planet?.sun?.setDateTime(d);
}
this.events.dispatch(this.events.setcurrent, d);
});
this._timelineView.events.on("changelocaltime", (isActive: boolean) => {
let sun = this.planet?.sun;
if (sun) {
if (isActive) {
this.applyLocalDateTime();
} else {
sun.setDateTime(this._timelineView.model.current);
this.renderer && this.renderer.requestRedraw();
}
}
this.events.dispatch(this.events.changelocaltime, isActive);
});
this._timelineView.events.on("changelocaldatetime", (d: Date) => {
this.applyLocalDateTime();
this.events.dispatch(this.events.changelocaldatetime, d);
});
this._timelineView.model.events.on("change", (...args: unknown[]) => {
this.events.dispatch(this.events.change, ...args);
});
this._timelineView.model.events.on("current", (d: Date) => {
this.events.dispatch(this.events.current, d);
});
this._timelineView.events.on("play", (...args: unknown[]) => {
defaultClock.multiplier = this._timelineView.model.multiplier;
defaultClock.start();
this.events.dispatch(this.events.play, ...args);
});
this._timelineView.events.on("playback", (...args: unknown[]) => {
defaultClock.multiplier = this._timelineView.model.multiplier;
defaultClock.start();
this.events.dispatch(this.events.playback, ...args);
});
this._timelineView.events.on("pause", (...args: unknown[]) => {
defaultClock.stop();
this.events.dispatch(this.events.pause, ...args);
});
this._timelineView.events.on("reset", (...args: unknown[]) => {
defaultClock.stop();
this.events.dispatch(this.events.reset, ...args);
});
this._timelineView.events.on("startdrag", (e: Event) => {
this._defaultClockWasRunningBeforeDrag = !this._timelineView.model.stopped();
defaultClock.stop();
this.planet?.sun!.stop();
this.renderer && this.renderer.controls.navigation.deactivate();
this.events.dispatch(this.events.startdrag, e);
});
this._timelineView.events.on("stopdrag", (d: Date) => {
if (this._defaultClockWasRunningBeforeDrag) {
defaultClock.start();
}
this.renderer && this.renderer.controls.navigation.activate();
this.events.dispatch(this.events.stopdrag, d);
});
this._timelineView.events.on("startdragcurrent", (e: Event) => {
this._defaultClockWasRunningBeforeDrag = !this._timelineView.model.stopped();
defaultClock.stop();
this.planet?.sun!.stop();
this.renderer && this.renderer.controls.navigation.deactivate();
this.events.dispatch(this.events.startdragcurrent, e);
});
this._timelineView.events.on("stopdragcurrent", (d: Date) => {
if (this._defaultClockWasRunningBeforeDrag) {
defaultClock.start();
}
this.renderer && this.renderer.controls.navigation.activate();
this.events.dispatch(this.events.stopdragcurrent, d);
});
}
}
export { TimelineControl };