UNPKG

terriajs

Version:

Geospatial data visualization platform.

502 lines (442 loc) 15.3 kB
import { action, computed, runInAction, makeObservable, override } from "mobx"; import binarySearch from "terriajs-cesium/Source/Core/binarySearch"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import { ChartPoint } from "../Charts/ChartData"; import getChartColorForId from "../Charts/getChartColorForId"; import AbstractConstructor from "../Core/AbstractConstructor"; import filterOutUndefined from "../Core/filterOutUndefined"; import isDefined from "../Core/isDefined"; import TerriaError from "../Core/TerriaError"; import ChartableMixin, { calculateDomain, ChartItem } from "../ModelMixins/ChartableMixin"; import CommonStrata from "../Models/Definition/CommonStrata"; import Model from "../Models/Definition/Model"; import DiscretelyTimeVaryingTraits from "../Traits/TraitsClasses/DiscretelyTimeVaryingTraits"; import TimeVarying, { DATE_SECONDS_PRECISION } from "./TimeVarying"; export interface AsJulian { time: JulianDate; tag: string; } export interface DiscreteTimeAsJS { time: string; tag: string | undefined; } function DiscretelyTimeVaryingMixin< T extends AbstractConstructor<Model<DiscretelyTimeVaryingTraits>> >(Base: T) { abstract class DiscretelyTimeVaryingMixin extends ChartableMixin(Base) implements TimeVarying { constructor(...args: any[]) { super(...args); makeObservable(this); } get hasDiscreteTimes() { return true; } abstract get discreteTimes(): DiscreteTimeAsJS[] | undefined; @override get currentTime(): string | undefined { const time = super.currentTime; if (time === undefined || time === null) { if (this.initialTimeSource === "now") { return JulianDate.toIso8601(JulianDate.now(), DATE_SECONDS_PRECISION); } else if (this.initialTimeSource === "start") { return this.startTime; } else if (this.initialTimeSource === "stop") { return this.stopTime; } else if (this.initialTimeSource === "none") { return undefined; } else { throw new TerriaError({ sender: this, title: "Invalid initialTime value", message: "The `initialTime` property has an invalid value: `" + this.initialTimeSource + "`." }); } } return time; } @computed({ equals: JulianDate.equals }) get currentTimeAsJulianDate() { return toJulianDate(this.currentTime); } @computed({ equals: JulianDate.equals }) get startTimeAsJulianDate(): JulianDate | undefined { return toJulianDate(this.startTime); } @computed({ equals: JulianDate.equals }) get stopTimeAsJulianDate(): JulianDate | undefined { return toJulianDate(this.stopTime); } @computed get objectifiedDates(): ObjectifiedDates { if (!isDefined(this.discreteTimesAsSortedJulianDates)) { return { index: [], dates: [] }; } const jsDates = this.discreteTimesAsSortedJulianDates.map((julianDate) => JulianDate.toDate(julianDate.time) ); return objectifyDates(jsDates); } @computed get discreteTimesAsSortedJulianDates(): AsJulian[] | undefined { const discreteTimes = this.discreteTimes; if (discreteTimes === undefined) { return undefined; } const asJulian: AsJulian[] = []; for (let i = 0; i < discreteTimes.length; i++) { const dt = discreteTimes[i]; try { if (dt.time !== undefined) { const time = JulianDate.fromIso8601(dt.time); asJulian.push({ time, tag: dt.tag !== undefined ? dt.tag : dt.time }); } } catch {} } asJulian.sort((a, b) => JulianDate.compare(a.time, b.time)); return asJulian; } getDiscreteTimeIndex(time: JulianDate): number | undefined { const discreteTimes = this.discreteTimesAsSortedJulianDates; if (discreteTimes === undefined || discreteTimes.length === 0) { return undefined; } // Where does `time` fit in our sequence of discrete times? const exactIndex = binarySearch( discreteTimes, time, (candidate, currentTime) => JulianDate.compare(candidate.time, currentTime) ); // We have this exact time in our discrete times if (exactIndex >= 0) { return exactIndex; } // This is where `time` could be inserted into the discrete times list so that they're all in sorted order const nextIndex = ~exactIndex; if (nextIndex === 0 || this.fromContinuous === "next") { // Before the first, or we want the next time no matter which is closest return nextIndex; } else if ( nextIndex === discreteTimes.length || this.fromContinuous === "previous" ) { // After the last, or we want the previous time no matter which is closest return nextIndex - 1; } else { // Get the closest discrete time const previousTime = discreteTimes[nextIndex - 1].time; const nextTime = discreteTimes[nextIndex].time; const timeFromPrevious = JulianDate.secondsDifference( time, previousTime ); const timeToNext = JulianDate.secondsDifference(nextTime, time); if (timeToNext > timeFromPrevious) { return nextIndex - 1; } else { return nextIndex; } } } @computed get currentDiscreteTimeIndex(): number | undefined { return ( this.currentTimeAsJulianDate && this.getDiscreteTimeIndex(this.currentTimeAsJulianDate) ); } @computed get nextDiscreteTimeIndex(): number | undefined { const index = this.currentDiscreteTimeIndex; if ( index === undefined || index === this.discreteTimesAsSortedJulianDates!.length - 1 ) { return undefined; } return index + 1; } @computed get previousDiscreteTimeIndex(): number | undefined { const index = this.currentDiscreteTimeIndex; if (index === undefined || index === 0) { return undefined; } return index - 1; } @computed({ equals: JulianDate.equals }) get currentDiscreteJulianDate() { const index = this.currentDiscreteTimeIndex; return index === undefined ? undefined : this.discreteTimesAsSortedJulianDates![index].time; } @computed({ equals: JulianDate.equals }) get nextDiscreteJulianDate() { const index = this.nextDiscreteTimeIndex; return index === undefined ? undefined : this.discreteTimesAsSortedJulianDates![index].time; } @computed get currentDiscreteTimeTag() { const index = this.currentDiscreteTimeIndex; return index === undefined ? undefined : this.discreteTimesAsSortedJulianDates![index].tag; } @computed get previousDiscreteTimeTag() { const index = this.previousDiscreteTimeIndex; return index === undefined ? undefined : this.discreteTimesAsSortedJulianDates![index].tag; } @computed get nextDiscreteTimeTag() { const index = this.nextDiscreteTimeIndex; return index === undefined ? undefined : this.discreteTimesAsSortedJulianDates![index].tag; } @computed get isPreviousDiscreteTimeAvailable(): boolean { return this.previousDiscreteTimeIndex !== undefined; } @computed get isNextDiscreteTimeAvailable(): boolean { return this.nextDiscreteTimeIndex !== undefined; } @override get startTime(): string | undefined { const time = super.startTime; if ( time === undefined && this.discreteTimesAsSortedJulianDates && this.discreteTimesAsSortedJulianDates.length > 0 ) { return JulianDate.toIso8601( this.discreteTimesAsSortedJulianDates[0].time, DATE_SECONDS_PRECISION ); } return time; } @override get stopTime(): string | undefined { const time = super.stopTime; if ( time === undefined && this.discreteTimesAsSortedJulianDates && this.discreteTimesAsSortedJulianDates.length > 0 ) { return JulianDate.toIso8601( this.discreteTimesAsSortedJulianDates[ this.discreteTimesAsSortedJulianDates.length - 1 ].time, DATE_SECONDS_PRECISION ); } return time; } /** * Try to calculate a multiplier which results in a new time step every {this.multiplierDefaultDeltaStep} seconds. For example, if {this.multiplierDefaultDeltaStep = 5} it would set the `multiplier` so that a new time step (of this dataset) would appear every five seconds (on average) if the timeline is playing. */ @override get multiplier() { if (super.multiplier) return super.multiplier; if ( !isDefined(this.startTimeAsJulianDate) || !isDefined(this.stopTimeAsJulianDate) || !isDefined(this.multiplierDefaultDeltaStep) || !isDefined(this.discreteTimesAsSortedJulianDates) ) return; const dSeconds = (this.stopTimeAsJulianDate.dayNumber - this.startTimeAsJulianDate.dayNumber) * 24 * 60 * 60 + this.stopTimeAsJulianDate.secondsOfDay - this.startTimeAsJulianDate.secondsOfDay; const meanDSeconds = dSeconds / this.discreteTimesAsSortedJulianDates.length; return meanDSeconds / this.multiplierDefaultDeltaStep; } @action moveToPreviousDiscreteTime(stratumId: string) { const index = this.previousDiscreteTimeIndex; if (index === undefined) { return; } this.setTrait( stratumId, "currentTime", JulianDate.toIso8601( this.discreteTimesAsSortedJulianDates![index].time, DATE_SECONDS_PRECISION ) ); } @action moveToNextDiscreteTime(stratumId: string) { const index = this.nextDiscreteTimeIndex; if (index === undefined) { return; } this.setTrait( stratumId, "currentTime", JulianDate.toIso8601( this.discreteTimesAsSortedJulianDates![index].time, DATE_SECONDS_PRECISION ) ); } @computed get momentChart(): ChartItem | undefined { if (!this.showInChartPanel || !this.discreteTimesAsSortedJulianDates) return; const points: ChartPoint[] = this.discreteTimesAsSortedJulianDates.map( (dt) => ({ x: JulianDate.toDate(dt.time), y: 0.5, isSelected: this.currentDiscreteJulianDate && this.currentDiscreteJulianDate.equals(dt.time) }) ); const colorId = `color-${this.name}`; return { item: this, id: this.name || "", name: this.name || "", categoryName: this.name, key: `key${this.uniqueId}-${this.name}`, type: this.chartType || "momentLines", glyphStyle: this.chartGlyphStyle, xAxis: { name: "Time", scale: "time" }, points, domain: { ...calculateDomain(points), y: [0, 1] }, showInChartPanel: this.show && this.showInChartPanel, isSelectedInWorkbench: this.showInChartPanel, updateIsSelectedInWorkbench: (isSelected: boolean) => { runInAction(() => { this.setTrait(CommonStrata.user, "showInChartPanel", isSelected); }); }, getColor: () => { return this.chartColor ? this.chartColor : getChartColorForId(colorId); }, onClick: (point: any) => { runInAction(() => { this.setTrait( CommonStrata.user, "currentTime", point.x.toISOString() ); }); } }; } @computed get chartItems(): ChartItem[] { return filterOutUndefined([this.momentChart]); } } return DiscretelyTimeVaryingMixin; } namespace DiscretelyTimeVaryingMixin { export interface Instance extends InstanceType< ReturnType<typeof DiscretelyTimeVaryingMixin> > {} export function isMixedInto(model: any): model is Instance { return model && model.hasDiscreteTimes; } } export default DiscretelyTimeVaryingMixin; function toJulianDate(time: string | undefined): JulianDate | undefined { if (time === undefined || time === null) { return undefined; } // JS's data parser produces some bizarre dates from bad strings without complaint, so we need to do some basic validation if (time.includes("NaN")) { return undefined; } const julianDate = JulianDate.fromIso8601(time); // Don't return an invalid JulianDate if (isNaN(julianDate.secondsOfDay) || isNaN(julianDate.dayNumber)) return undefined; return julianDate; } type DatesObject<T> = { [key: number]: T; dates: Date[]; index: number[]; }; export type ObjectifiedDates = DatesObject<ObjectifiedYears>; export type ObjectifiedYears = DatesObject<ObjectifiedMonths>; export type ObjectifiedMonths = DatesObject<ObjectifiedDays>; export type ObjectifiedDays = DatesObject<ObjectifiedHours>; export type ObjectifiedHours = DatesObject<Date[]>; /** * Process an array of dates into layered objects of years, months and days. * @param {Date[]} An array of dates. * @return {Object} Returns an object whose keys are years, whose values are objects whose keys are months (0=Jan), * whose values are objects whose keys are days, whose values are arrays of all the datetimes on that day. */ export function objectifyDates(dates: Date[]): ObjectifiedDates { const result: ObjectifiedDates = { index: [], dates }; for (let i = 0; i < dates.length; i++) { const date = dates[i]; const year = date.getFullYear(); const century = Math.floor(year / 100); const month = date.getMonth(); const day = date.getDate(); const hour = date.getHours(); // ObjectifiedDates if (!result[century]) { result[century] = { index: [], dates: [] }; result.index.push(century); } result[century].dates.push(date); // ObjectifiedYears if (!result[century][year]) { result[century][year] = { index: [], dates: [] }; result[century].index.push(year); } result[century][year].dates.push(date); // ObjectifiedMonths if (!result[century][year][month]) { result[century][year][month] = { index: [], dates: [] }; result[century][year].index.push(month); } result[century][year][month].dates.push(date); // ObjectifiedDays if (!result[century][year][month][day]) { result[century][year][month][day] = { index: [], dates: [] }; result[century][year][month].index.push(day); } result[century][year][month][day].dates.push(date); // ObjectifiedHours if (!result[century][year][month][day][hour]) { result[century][year][month][day][hour] = []; result[century][year][month][day].index.push(hour); } result[century][year][month][day][hour].push(date); } return result; }