UNPKG

terriajs

Version:

Geospatial data visualization platform.

286 lines (260 loc) 8.3 kB
import { IReactionDisposer, action, autorun, computed, makeObservable, observable } from "mobx"; import Clock from "terriajs-cesium/Source/Core/Clock"; import ClockRange from "terriajs-cesium/Source/Core/ClockRange"; import CesiumEvent from "terriajs-cesium/Source/Core/Event"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import filterOutUndefined from "../Core/filterOutUndefined"; import ReferenceMixin from "../ModelMixins/ReferenceMixin"; import TimeVarying, { DATE_SECONDS_PRECISION } from "../ModelMixins/TimeVarying"; import DefaultTimelineModel from "./DefaultTimelineModel"; import CommonStrata from "./Definition/CommonStrata"; import Terria from "./Terria"; const DEFAULT_TIMELINE_MODEL_ID = "defaultTimeline"; /** * Manages a stack of all the time-varying datasets currently attached to the timeline. Provides * access to the current top dataset so that it can be displayed to the user. * * @constructor */ export default class TimelineStack { /** * The stratum of each layer in the stack in which to store the current time as the clock ticks. */ tickStratumId: string = CommonStrata.user; @observable items: TimeVarying[] = []; @observable defaultTimeVarying: TimeVarying | undefined; private _disposeClockAutorun: IReactionDisposer | undefined; private _disposeTickSubscription: CesiumEvent.RemoveCallback | undefined; constructor( readonly terria: Terria, readonly clock: Clock ) { makeObservable(this); } activate(): void { // Keep the Cesium clock in sync with the top layer's clock. this._disposeClockAutorun = autorun(() => { const topLayer = this.top; if (!topLayer || !topLayer.currentTimeAsJulianDate) { this.clock.shouldAnimate = false; return; } this.clock.currentTime = JulianDate.clone( topLayer.currentTimeAsJulianDate, this.clock.currentTime ); this.clock.startTime = offsetIfUndefined( -43200.0, topLayer.currentTimeAsJulianDate, topLayer.startTimeAsJulianDate, this.clock.startTime ); this.clock.stopTime = offsetIfUndefined( 43200.0, topLayer.currentTimeAsJulianDate, topLayer.stopTimeAsJulianDate, this.clock.stopTime ); if (topLayer.multiplier !== undefined) { this.clock.multiplier = topLayer.multiplier; } else { this.clock.multiplier = 60.0; } this.clock.shouldAnimate = !topLayer.isPaused; this.clock.clockRange = ClockRange.LOOP_STOP; if (this._disposeTickSubscription === undefined) { // We should start synchronizing only after first run of this autorun so that // the clock parameters are set correctly. this._disposeTickSubscription = this.clock.onTick.addEventListener( () => { this.syncToClock(this.tickStratumId); } ); } }); } deactivate(): void { if (this._disposeClockAutorun) { this._disposeClockAutorun(); } if (this._disposeTickSubscription) { this._disposeTickSubscription(); this._disposeTickSubscription = undefined; } } /** * The topmost time-series layer, or undefined if there is no such layer in the stack. */ @computed get top(): TimeVarying | undefined { // Find the first item with a current, start, and stop time. // Use the default if there isn't one. return ( this.items.find((item) => { const dereferenced: TimeVarying = ReferenceMixin.isMixedInto(item) && item.target ? (item.target as TimeVarying) : item; return ( dereferenced.currentTimeAsJulianDate !== undefined && dereferenced.startTimeAsJulianDate !== undefined && dereferenced.stopTimeAsJulianDate !== undefined ); }) || this.defaultTimeVarying ); } @computed get itemIds(): readonly string[] { return filterOutUndefined(this.items.map((item) => item.uniqueId)); } /** * Determines if the stack contains a given item. * @param item The item to check. * @returns True if the stack contains the item; otherwise, false. */ contains(item: TimeVarying): boolean { return this.items.indexOf(item) >= 0; } /** * Adds the supplied {@link TimeVarying} to the top of the stack. If the item is already in the stack, it will be moved * rather than added twice. * * @param item */ @action addToTop(item: TimeVarying): void { const currentIndex = this.items.indexOf(item); this.items.unshift(item); if (currentIndex > -1) { this.items.splice(currentIndex, 1); } } /** * Removes a layer from the stack, no matter what its location. If the layer is currently at the top, the value of * {@link TimelineStack#topLayer} will change. * * @param item; */ @action remove(item: TimeVarying): void { const index = this.items.indexOf(item); this.items.splice(index, 1); } /** * Removes all layers. */ @action removeAll(): void { this.items = []; } /** * Promotes the supplied {@link CatalogItem} to the top of the stack if it is already in the stack. If the item is not * already in the stack it won't be added. * * @param item */ @action promoteToTop(item: TimeVarying): void { const currentIndex = this.items.indexOf(item); if (currentIndex > -1) { this.addToTop(item); } } /** * Synchronizes all layers in the stack to the current time and the paused state of the provided clock. * Synchronizes the {@link TimelineStack#top} to the clock's `startTime`, `endTime`, and `multiplier`. * @param stratumId The stratum in which to modify properties. * @param clock The clock to sync to. */ @action syncToClock(stratumId: string): void { const clock = this.clock; const currentTime = JulianDate.toIso8601( clock.currentTime, DATE_SECONDS_PRECISION ); const isPaused = !clock.shouldAnimate; if (this.top) { this.top.setTrait( stratumId, "startTime", JulianDate.toIso8601(clock.startTime, DATE_SECONDS_PRECISION) ); this.top.setTrait( stratumId, "stopTime", JulianDate.toIso8601(clock.stopTime, DATE_SECONDS_PRECISION) ); this.top.setTrait(stratumId, "multiplier", clock.multiplier); } for (let i = 0; i < this.items.length; ++i) { const layer = this.items[i]; layer.setTrait(stratumId, "currentTime", currentTime); layer.setTrait(stratumId, "isPaused", isPaused); } if (this.defaultTimeVarying) { this.defaultTimeVarying.setTrait(stratumId, "currentTime", currentTime); this.defaultTimeVarying.setTrait(stratumId, "isPaused", isPaused); } } @action setAlwaysShowTimeline(show = true): void { if (show) { this.defaultTimeVarying = this.getOrCreateDefaultTimelineModel(); } else { if (this.defaultTimeVarying) { // Unregister the model so that it doesn't appear in share links this.terria.removeModelReferences(this.defaultTimeVarying); } this.defaultTimeVarying = undefined; } this.terria.currentViewer.notifyRepaintRequired(); } @computed get alwaysShowingTimeline() { return ( this.defaultTimeVarying !== undefined && this.defaultTimeVarying.startTimeAsJulianDate !== undefined && this.defaultTimeVarying.stopTimeAsJulianDate !== undefined && this.defaultTimeVarying.currentTimeAsJulianDate !== undefined ); } private getOrCreateDefaultTimelineModel(): DefaultTimelineModel { let model = this.terria.getModelById( DefaultTimelineModel, DEFAULT_TIMELINE_MODEL_ID ); if (!model) { model = new DefaultTimelineModel(DEFAULT_TIMELINE_MODEL_ID, this.terria); this.terria.addModel(model); } return model; } } function offsetIfUndefined( offsetSeconds: number, baseTime: JulianDate, time: JulianDate | undefined, result?: JulianDate ): JulianDate { if (time === undefined) { return JulianDate.addSeconds( baseTime, offsetSeconds, result || new JulianDate() ); } else { return JulianDate.clone(time, result); } }