Models/Clock.js

"use strict";

var CesiumClock = require("terriajs-cesium/Source/Core/Clock").default;
var TimeInterval = require("terriajs-cesium/Source/Core/TimeInterval").default;
var inherit = require("../Core/inherit");
var defined = require("terriajs-cesium/Source/Core/defined").default;

/**
 * Extension of the standard Clock provided by Cesium customised to deal with Terria-level abstractions like
 * {@link CatalogItem}s.
 * @param options see {@link terriajs-cesium/Source/Core/Clock}
 * @constructor
 */
var Clock = function(options) {
  CesiumClock.call(this, options);

  var baseProperty = Object.getOwnPropertyDescriptor(
    CesiumClock.prototype,
    "currentTime"
  );
  Object.defineProperty(this, "currentTime", {
    get: function() {
      return baseProperty.get.call(this);
    },
    set: function(newTime) {
      var gap = this.determineGap(newTime);
      baseProperty.set.call(this, defined(gap) ? gap.stop : newTime);
    }
  });

  this.onTick.addEventListener(function() {
    var newTime = this.currentTime;
    var gap = this.determineGap(newTime);
    this._currentTime = defined(gap) ? gap.stop : newTime;
  }, this);
};

inherit(CesiumClock, Clock);

/**
 * Set the {@link CatalogItem} that this clock is currently tracking.
 *
 * @param {CatalogItem} item
 */
Clock.prototype.setCatalogItem = function(item) {
  this.catalogItem = item;
  // The next line "gets" the value of item.clock into "this".
  // Ie. it copies startTime, stopTime, currentTime, clockRange, multiplier and clockStep from the datasource clock onto "this"
  // (which is typically the terria.clock instance).
  item.clock.getValue(this);
};

/**
 * Determines whether the passed {@link JulianDate} occurs within a gap in the currently tracked {@link CatalogItem}'s
 * data. If so returns that gap as a {@link TimeInterval}, otherwise undefined.
 *
 * @param {JulianDate} date
 * @returns {TimeInterval} The gap in the data that the passed date falls within, if one exists.
 */
Clock.prototype.determineGap = function(date) {
  var intervals = this.catalogItem && this.catalogItem.intervals;

  if (defined(intervals)) {
    var matchingIntervalIndex = intervals.indexOf(date);

    // If an index can't be found, TimeIntervalCollection returns the (negative) bitwise complement of the next index after
    // where the date would fit...
    if (matchingIntervalIndex >= 0) {
      // ... so if it's >= 0 our date actually has an interval specified for it, return undefined.
      return;
    }

    // ... otherwise flip the bits back and use the index to figure out where the boundaries of our gap are.
    var gapEndIndex = ~matchingIntervalIndex;
    var gapStart, gapStop;
    if (gapEndIndex === 0) {
      gapStart = this.catalogItem.clock.startTime;
      gapStop = intervals.get(0).start;
    } else if (gapEndIndex === intervals.length) {
      gapStop = this.catalogItem.clock.stopTime;
    }

    return new TimeInterval({
      // using || instead of defaultValue because we don't want the default to execute if gapStart is truthy.
      start: gapStart || intervals.get(gapEndIndex - 1).stop,
      stop: gapStop || intervals.get(gapEndIndex).start,
      isStartIncluded: false,
      isStopIncluded: false
    });
  }
};

Clock.prototype.tick = function() {
  CesiumClock.prototype.tick.call(this);

  // Return this._currentTime instead of whatever the base class tick returns.
  // This way we return the gap-jumped time.
  return this._currentTime;
};

module.exports = Clock;