import binarySearch from "../../Core/binarySearch.js";
|
import ClockRange from "../../Core/ClockRange.js";
|
import ClockStep from "../../Core/ClockStep.js";
|
import defined from "../../Core/defined.js";
|
import DeveloperError from "../../Core/DeveloperError.js";
|
import JulianDate from "../../Core/JulianDate.js";
|
import knockout from "../../ThirdParty/knockout.js";
|
import createCommand from "../createCommand.js";
|
import ToggleButtonViewModel from "../ToggleButtonViewModel.js";
|
|
var monthNames = [
|
"Jan",
|
"Feb",
|
"Mar",
|
"Apr",
|
"May",
|
"Jun",
|
"Jul",
|
"Aug",
|
"Sep",
|
"Oct",
|
"Nov",
|
"Dec",
|
];
|
var realtimeShuttleRingAngle = 15;
|
var maxShuttleRingAngle = 105;
|
|
function numberComparator(left, right) {
|
return left - right;
|
}
|
|
function getTypicalMultiplierIndex(multiplier, shuttleRingTicks) {
|
var index = binarySearch(shuttleRingTicks, multiplier, numberComparator);
|
return index < 0 ? ~index : index;
|
}
|
|
function angleToMultiplier(angle, shuttleRingTicks) {
|
//Use a linear scale for -1 to 1 between -15 < angle < 15 degrees
|
if (Math.abs(angle) <= realtimeShuttleRingAngle) {
|
return angle / realtimeShuttleRingAngle;
|
}
|
|
var minp = realtimeShuttleRingAngle;
|
var maxp = maxShuttleRingAngle;
|
var maxv;
|
var minv = 0;
|
var scale;
|
if (angle > 0) {
|
maxv = Math.log(shuttleRingTicks[shuttleRingTicks.length - 1]);
|
scale = (maxv - minv) / (maxp - minp);
|
return Math.exp(minv + scale * (angle - minp));
|
}
|
|
maxv = Math.log(-shuttleRingTicks[0]);
|
scale = (maxv - minv) / (maxp - minp);
|
return -Math.exp(minv + scale * (Math.abs(angle) - minp));
|
}
|
|
function multiplierToAngle(multiplier, shuttleRingTicks, clockViewModel) {
|
if (clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK) {
|
return realtimeShuttleRingAngle;
|
}
|
|
if (Math.abs(multiplier) <= 1) {
|
return multiplier * realtimeShuttleRingAngle;
|
}
|
|
var fastedMultipler = shuttleRingTicks[shuttleRingTicks.length - 1];
|
if (multiplier > fastedMultipler) {
|
multiplier = fastedMultipler;
|
} else if (multiplier < -fastedMultipler) {
|
multiplier = -fastedMultipler;
|
}
|
|
var minp = realtimeShuttleRingAngle;
|
var maxp = maxShuttleRingAngle;
|
var maxv;
|
var minv = 0;
|
var scale;
|
|
if (multiplier > 0) {
|
maxv = Math.log(fastedMultipler);
|
scale = (maxv - minv) / (maxp - minp);
|
return (Math.log(multiplier) - minv) / scale + minp;
|
}
|
|
maxv = Math.log(-shuttleRingTicks[0]);
|
scale = (maxv - minv) / (maxp - minp);
|
return -((Math.log(Math.abs(multiplier)) - minv) / scale + minp);
|
}
|
|
/**
|
* The view model for the {@link Animation} widget.
|
* @alias AnimationViewModel
|
* @constructor
|
*
|
* @param {ClockViewModel} clockViewModel The ClockViewModel instance to use.
|
*
|
* @see Animation
|
*/
|
function AnimationViewModel(clockViewModel) {
|
//>>includeStart('debug', pragmas.debug);
|
if (!defined(clockViewModel)) {
|
throw new DeveloperError("clockViewModel is required.");
|
}
|
//>>includeEnd('debug');
|
|
var that = this;
|
this._clockViewModel = clockViewModel;
|
this._allShuttleRingTicks = [];
|
this._dateFormatter = AnimationViewModel.defaultDateFormatter;
|
this._timeFormatter = AnimationViewModel.defaultTimeFormatter;
|
|
/**
|
* Gets or sets whether the shuttle ring is currently being dragged. This property is observable.
|
* @type {Boolean}
|
* @default false
|
*/
|
this.shuttleRingDragging = false;
|
|
/**
|
* Gets or sets whether dragging the shuttle ring should cause the multiplier
|
* to snap to the defined tick values rather than interpolating between them.
|
* This property is observable.
|
* @type {Boolean}
|
* @default false
|
*/
|
this.snapToTicks = false;
|
|
knockout.track(this, [
|
"_allShuttleRingTicks",
|
"_dateFormatter",
|
"_timeFormatter",
|
"shuttleRingDragging",
|
"snapToTicks",
|
]);
|
|
this._sortedFilteredPositiveTicks = [];
|
|
this.setShuttleRingTicks(AnimationViewModel.defaultTicks);
|
|
/**
|
* Gets the string representation of the current time. This property is observable.
|
* @type {String}
|
*/
|
this.timeLabel = undefined;
|
knockout.defineProperty(this, "timeLabel", function () {
|
return that._timeFormatter(that._clockViewModel.currentTime, that);
|
});
|
|
/**
|
* Gets the string representation of the current date. This property is observable.
|
* @type {String}
|
*/
|
this.dateLabel = undefined;
|
knockout.defineProperty(this, "dateLabel", function () {
|
return that._dateFormatter(that._clockViewModel.currentTime, that);
|
});
|
|
/**
|
* Gets the string representation of the current multiplier. This property is observable.
|
* @type {String}
|
*/
|
this.multiplierLabel = undefined;
|
knockout.defineProperty(this, "multiplierLabel", function () {
|
var clockViewModel = that._clockViewModel;
|
if (clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK) {
|
return "Today";
|
}
|
|
var multiplier = clockViewModel.multiplier;
|
|
//If it's a whole number, just return it.
|
if (multiplier % 1 === 0) {
|
return multiplier.toFixed(0) + "x";
|
}
|
|
//Convert to decimal string and remove any trailing zeroes
|
return multiplier.toFixed(3).replace(/0{0,3}$/, "") + "x";
|
});
|
|
/**
|
* Gets or sets the current shuttle ring angle. This property is observable.
|
* @type {Number}
|
*/
|
this.shuttleRingAngle = undefined;
|
knockout.defineProperty(this, "shuttleRingAngle", {
|
get: function () {
|
return multiplierToAngle(
|
clockViewModel.multiplier,
|
that._allShuttleRingTicks,
|
clockViewModel
|
);
|
},
|
set: function (angle) {
|
angle = Math.max(
|
Math.min(angle, maxShuttleRingAngle),
|
-maxShuttleRingAngle
|
);
|
var ticks = that._allShuttleRingTicks;
|
|
var clockViewModel = that._clockViewModel;
|
clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
|
|
//If we are at the max angle, simply return the max value in either direction.
|
if (Math.abs(angle) === maxShuttleRingAngle) {
|
clockViewModel.multiplier =
|
angle > 0 ? ticks[ticks.length - 1] : ticks[0];
|
return;
|
}
|
|
var multiplier = angleToMultiplier(angle, ticks);
|
if (that.snapToTicks) {
|
multiplier = ticks[getTypicalMultiplierIndex(multiplier, ticks)];
|
} else if (multiplier !== 0) {
|
var positiveMultiplier = Math.abs(multiplier);
|
|
if (positiveMultiplier > 100) {
|
var numDigits = positiveMultiplier.toFixed(0).length - 2;
|
var divisor = Math.pow(10, numDigits);
|
multiplier = (Math.round(multiplier / divisor) * divisor) | 0;
|
} else if (positiveMultiplier > realtimeShuttleRingAngle) {
|
multiplier = Math.round(multiplier);
|
} else if (positiveMultiplier > 1) {
|
multiplier = +multiplier.toFixed(1);
|
} else if (positiveMultiplier > 0) {
|
multiplier = +multiplier.toFixed(2);
|
}
|
}
|
clockViewModel.multiplier = multiplier;
|
},
|
});
|
|
this._canAnimate = undefined;
|
knockout.defineProperty(this, "_canAnimate", function () {
|
var clockViewModel = that._clockViewModel;
|
var clockRange = clockViewModel.clockRange;
|
|
if (that.shuttleRingDragging || clockRange === ClockRange.UNBOUNDED) {
|
return true;
|
}
|
|
var multiplier = clockViewModel.multiplier;
|
var currentTime = clockViewModel.currentTime;
|
var startTime = clockViewModel.startTime;
|
|
var result = false;
|
if (clockRange === ClockRange.LOOP_STOP) {
|
result =
|
JulianDate.greaterThan(currentTime, startTime) ||
|
(currentTime.equals(startTime) && multiplier > 0);
|
} else {
|
var stopTime = clockViewModel.stopTime;
|
result =
|
(JulianDate.greaterThan(currentTime, startTime) &&
|
JulianDate.lessThan(currentTime, stopTime)) || //
|
(currentTime.equals(startTime) && multiplier > 0) || //
|
(currentTime.equals(stopTime) && multiplier < 0);
|
}
|
|
if (!result) {
|
clockViewModel.shouldAnimate = false;
|
}
|
return result;
|
});
|
|
this._isSystemTimeAvailable = undefined;
|
knockout.defineProperty(this, "_isSystemTimeAvailable", function () {
|
var clockViewModel = that._clockViewModel;
|
var clockRange = clockViewModel.clockRange;
|
if (clockRange === ClockRange.UNBOUNDED) {
|
return true;
|
}
|
|
var systemTime = clockViewModel.systemTime;
|
return (
|
JulianDate.greaterThanOrEquals(systemTime, clockViewModel.startTime) &&
|
JulianDate.lessThanOrEquals(systemTime, clockViewModel.stopTime)
|
);
|
});
|
|
this._isAnimating = undefined;
|
knockout.defineProperty(this, "_isAnimating", function () {
|
return (
|
that._clockViewModel.shouldAnimate &&
|
(that._canAnimate || that.shuttleRingDragging)
|
);
|
});
|
|
var pauseCommand = createCommand(function () {
|
var clockViewModel = that._clockViewModel;
|
if (clockViewModel.shouldAnimate) {
|
clockViewModel.shouldAnimate = false;
|
} else if (that._canAnimate) {
|
clockViewModel.shouldAnimate = true;
|
}
|
});
|
|
this._pauseViewModel = new ToggleButtonViewModel(pauseCommand, {
|
toggled: knockout.computed(function () {
|
return !that._isAnimating;
|
}),
|
tooltip: "Pause",
|
});
|
|
var playReverseCommand = createCommand(function () {
|
var clockViewModel = that._clockViewModel;
|
var multiplier = clockViewModel.multiplier;
|
if (multiplier > 0) {
|
clockViewModel.multiplier = -multiplier;
|
}
|
clockViewModel.shouldAnimate = true;
|
});
|
|
this._playReverseViewModel = new ToggleButtonViewModel(playReverseCommand, {
|
toggled: knockout.computed(function () {
|
return that._isAnimating && clockViewModel.multiplier < 0;
|
}),
|
tooltip: "Play Reverse",
|
});
|
|
var playForwardCommand = createCommand(function () {
|
var clockViewModel = that._clockViewModel;
|
var multiplier = clockViewModel.multiplier;
|
if (multiplier < 0) {
|
clockViewModel.multiplier = -multiplier;
|
}
|
clockViewModel.shouldAnimate = true;
|
});
|
|
this._playForwardViewModel = new ToggleButtonViewModel(playForwardCommand, {
|
toggled: knockout.computed(function () {
|
return (
|
that._isAnimating &&
|
clockViewModel.multiplier > 0 &&
|
clockViewModel.clockStep !== ClockStep.SYSTEM_CLOCK
|
);
|
}),
|
tooltip: "Play Forward",
|
});
|
|
var playRealtimeCommand = createCommand(function () {
|
that._clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK;
|
}, knockout.getObservable(this, "_isSystemTimeAvailable"));
|
|
this._playRealtimeViewModel = new ToggleButtonViewModel(playRealtimeCommand, {
|
toggled: knockout.computed(function () {
|
return clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK;
|
}),
|
tooltip: knockout.computed(function () {
|
return that._isSystemTimeAvailable
|
? "Today (real-time)"
|
: "Current time not in range";
|
}),
|
});
|
|
this._slower = createCommand(function () {
|
var clockViewModel = that._clockViewModel;
|
var shuttleRingTicks = that._allShuttleRingTicks;
|
var multiplier = clockViewModel.multiplier;
|
var index = getTypicalMultiplierIndex(multiplier, shuttleRingTicks) - 1;
|
if (index >= 0) {
|
clockViewModel.multiplier = shuttleRingTicks[index];
|
}
|
});
|
|
this._faster = createCommand(function () {
|
var clockViewModel = that._clockViewModel;
|
var shuttleRingTicks = that._allShuttleRingTicks;
|
var multiplier = clockViewModel.multiplier;
|
var index = getTypicalMultiplierIndex(multiplier, shuttleRingTicks) + 1;
|
if (index < shuttleRingTicks.length) {
|
clockViewModel.multiplier = shuttleRingTicks[index];
|
}
|
});
|
}
|
|
/**
|
* Gets or sets the default date formatter used by new instances.
|
*
|
* @member
|
* @type {AnimationViewModel.DateFormatter}
|
*/
|
AnimationViewModel.defaultDateFormatter = function (date, viewModel) {
|
var gregorianDate = JulianDate.toGregorianDate(date);
|
return (
|
monthNames[gregorianDate.month - 1] +
|
" " +
|
gregorianDate.day +
|
" " +
|
gregorianDate.year
|
);
|
};
|
|
/**
|
* Gets or sets the default array of known clock multipliers associated with new instances of the shuttle ring.
|
* @type {Number[]}
|
*/
|
AnimationViewModel.defaultTicks = [
|
//
|
0.001,
|
0.002,
|
0.005,
|
0.01,
|
0.02,
|
0.05,
|
0.1,
|
0.25,
|
0.5,
|
1.0,
|
2.0,
|
5.0,
|
10.0, //
|
15.0,
|
30.0,
|
60.0,
|
120.0,
|
300.0,
|
600.0,
|
900.0,
|
1800.0,
|
3600.0,
|
7200.0,
|
14400.0, //
|
21600.0,
|
43200.0,
|
86400.0,
|
172800.0,
|
345600.0,
|
604800.0,
|
];
|
|
/**
|
* Gets or sets the default time formatter used by new instances.
|
*
|
* @member
|
* @type {AnimationViewModel.TimeFormatter}
|
*/
|
AnimationViewModel.defaultTimeFormatter = function (date, viewModel) {
|
var gregorianDate = JulianDate.toGregorianDate(date);
|
var millisecond = Math.round(gregorianDate.millisecond);
|
if (Math.abs(viewModel._clockViewModel.multiplier) < 1) {
|
return (
|
gregorianDate.hour.toString().padStart(2, "0") +
|
":" +
|
gregorianDate.minute.toString().padStart(2, "0") +
|
":" +
|
gregorianDate.second.toString().padStart(2, "0") +
|
"." +
|
millisecond.toString().padStart(3, "0")
|
);
|
}
|
return (
|
gregorianDate.hour.toString().padStart(2, "0") +
|
":" +
|
gregorianDate.minute.toString().padStart(2, "0") +
|
":" +
|
gregorianDate.second.toString().padStart(2, "0") +
|
" UTC"
|
);
|
};
|
|
/**
|
* Gets a copy of the array of positive known clock multipliers to associate with the shuttle ring.
|
*
|
* @returns {Number[]} The array of known clock multipliers associated with the shuttle ring.
|
*/
|
AnimationViewModel.prototype.getShuttleRingTicks = function () {
|
return this._sortedFilteredPositiveTicks.slice(0);
|
};
|
|
/**
|
* Sets the array of positive known clock multipliers to associate with the shuttle ring.
|
* These values will have negative equivalents created for them and sets both the minimum
|
* and maximum range of values for the shuttle ring as well as the values that are snapped
|
* to when a single click is made. The values need not be in order, as they will be sorted
|
* automatically, and duplicate values will be removed.
|
*
|
* @param {Number[]} positiveTicks The list of known positive clock multipliers to associate with the shuttle ring.
|
*/
|
AnimationViewModel.prototype.setShuttleRingTicks = function (positiveTicks) {
|
//>>includeStart('debug', pragmas.debug);
|
if (!defined(positiveTicks)) {
|
throw new DeveloperError("positiveTicks is required.");
|
}
|
//>>includeEnd('debug');
|
|
var i;
|
var len;
|
var tick;
|
|
var hash = {};
|
var sortedFilteredPositiveTicks = this._sortedFilteredPositiveTicks;
|
sortedFilteredPositiveTicks.length = 0;
|
for (i = 0, len = positiveTicks.length; i < len; ++i) {
|
tick = positiveTicks[i];
|
//filter duplicates
|
if (!hash.hasOwnProperty(tick)) {
|
hash[tick] = true;
|
sortedFilteredPositiveTicks.push(tick);
|
}
|
}
|
sortedFilteredPositiveTicks.sort(numberComparator);
|
|
var allTicks = [];
|
for (len = sortedFilteredPositiveTicks.length, i = len - 1; i >= 0; --i) {
|
tick = sortedFilteredPositiveTicks[i];
|
if (tick !== 0) {
|
allTicks.push(-tick);
|
}
|
}
|
Array.prototype.push.apply(allTicks, sortedFilteredPositiveTicks);
|
|
this._allShuttleRingTicks = allTicks;
|
};
|
|
Object.defineProperties(AnimationViewModel.prototype, {
|
/**
|
* Gets a command that decreases the speed of animation.
|
* @memberof AnimationViewModel.prototype
|
* @type {Command}
|
*/
|
slower: {
|
get: function () {
|
return this._slower;
|
},
|
},
|
|
/**
|
* Gets a command that increases the speed of animation.
|
* @memberof AnimationViewModel.prototype
|
* @type {Command}
|
*/
|
faster: {
|
get: function () {
|
return this._faster;
|
},
|
},
|
|
/**
|
* Gets the clock view model.
|
* @memberof AnimationViewModel.prototype
|
*
|
* @type {ClockViewModel}
|
*/
|
clockViewModel: {
|
get: function () {
|
return this._clockViewModel;
|
},
|
},
|
|
/**
|
* Gets the pause toggle button view model.
|
* @memberof AnimationViewModel.prototype
|
*
|
* @type {ToggleButtonViewModel}
|
*/
|
pauseViewModel: {
|
get: function () {
|
return this._pauseViewModel;
|
},
|
},
|
|
/**
|
* Gets the reverse toggle button view model.
|
* @memberof AnimationViewModel.prototype
|
*
|
* @type {ToggleButtonViewModel}
|
*/
|
playReverseViewModel: {
|
get: function () {
|
return this._playReverseViewModel;
|
},
|
},
|
|
/**
|
* Gets the play toggle button view model.
|
* @memberof AnimationViewModel.prototype
|
*
|
* @type {ToggleButtonViewModel}
|
*/
|
playForwardViewModel: {
|
get: function () {
|
return this._playForwardViewModel;
|
},
|
},
|
|
/**
|
* Gets the realtime toggle button view model.
|
* @memberof AnimationViewModel.prototype
|
*
|
* @type {ToggleButtonViewModel}
|
*/
|
playRealtimeViewModel: {
|
get: function () {
|
return this._playRealtimeViewModel;
|
},
|
},
|
|
/**
|
* Gets or sets the function which formats a date for display.
|
* @memberof AnimationViewModel.prototype
|
*
|
* @type {AnimationViewModel.DateFormatter}
|
* @default AnimationViewModel.defaultDateFormatter
|
*/
|
dateFormatter: {
|
//TODO:@exception {DeveloperError} dateFormatter must be a function.
|
get: function () {
|
return this._dateFormatter;
|
},
|
set: function (dateFormatter) {
|
//>>includeStart('debug', pragmas.debug);
|
if (typeof dateFormatter !== "function") {
|
throw new DeveloperError("dateFormatter must be a function");
|
}
|
//>>includeEnd('debug');
|
|
this._dateFormatter = dateFormatter;
|
},
|
},
|
|
/**
|
* Gets or sets the function which formats a time for display.
|
* @memberof AnimationViewModel.prototype
|
*
|
* @type {AnimationViewModel.TimeFormatter}
|
* @default AnimationViewModel.defaultTimeFormatter
|
*/
|
timeFormatter: {
|
//TODO:@exception {DeveloperError} timeFormatter must be a function.
|
get: function () {
|
return this._timeFormatter;
|
},
|
set: function (timeFormatter) {
|
//>>includeStart('debug', pragmas.debug);
|
if (typeof timeFormatter !== "function") {
|
throw new DeveloperError("timeFormatter must be a function");
|
}
|
//>>includeEnd('debug');
|
|
this._timeFormatter = timeFormatter;
|
},
|
},
|
});
|
|
//Currently exposed for tests.
|
AnimationViewModel._maxShuttleRingAngle = maxShuttleRingAngle;
|
AnimationViewModel._realtimeShuttleRingAngle = realtimeShuttleRingAngle;
|
|
/**
|
* A function that formats a date for display.
|
* @callback AnimationViewModel.DateFormatter
|
*
|
* @param {JulianDate} date The date to be formatted
|
* @param {AnimationViewModel} viewModel The AnimationViewModel instance requesting formatting.
|
* @returns {String} The string representation of the calendar date portion of the provided date.
|
*/
|
|
/**
|
* A function that formats a time for display.
|
* @callback AnimationViewModel.TimeFormatter
|
*
|
* @param {JulianDate} date The date to be formatted
|
* @param {AnimationViewModel} viewModel The AnimationViewModel instance requesting formatting.
|
* @returns {String} The string representation of the time portion of the provided date.
|
*/
|
export default AnimationViewModel;
|