/*global define*/ define([ '../../Core/binarySearch', '../../Core/ClockRange', '../../Core/ClockStep', '../../Core/defined', '../../Core/defineProperties', '../../Core/DeveloperError', '../../Core/JulianDate', '../../ThirdParty/knockout', '../../ThirdParty/sprintf', '../createCommand', '../ToggleButtonViewModel' ], function( binarySearch, ClockRange, ClockStep, defined, defineProperties, DeveloperError, JulianDate, knockout, sprintf, createCommand, ToggleButtonViewModel) { "use strict"; var monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; var realtimeShuttleRingAngle = 15; var maxShuttleRingAngle = 105; function cancelRealtime(clockViewModel) { if (clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK) { clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER; clockViewModel.multiplier = 1; } } function unpause(clockViewModel) { cancelRealtime(clockViewModel); clockViewModel.shouldAnimate = true; } 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 minp = realtimeShuttleRingAngle; var maxp = maxShuttleRingAngle; var maxv; var minv = 0; var scale; if (multiplier > 0) { maxv = Math.log(shuttleRingTicks[shuttleRingTicks.length - 1]); 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 */ var AnimationViewModel = function(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) { cancelRealtime(clockViewModel); clockViewModel.shouldAnimate = false; } else if (that._canAnimate) { unpause(clockViewModel); } }); this._pauseViewModel = new ToggleButtonViewModel(pauseCommand, { toggled : knockout.computed(function() { return !that._isAnimating; }), tooltip : 'Pause' }); var playReverseCommand = createCommand(function() { var clockViewModel = that._clockViewModel; cancelRealtime(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; cancelRealtime(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() { var clockViewModel = that._clockViewModel; clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK; clockViewModel.multiplier = 1.0; clockViewModel.shouldAnimate = true; }, knockout.getObservable(this, '_isSystemTimeAvailable')); this._playRealtimeViewModel = new ToggleButtonViewModel(playRealtimeCommand, { toggled : knockout.computed(function() { return clockViewModel.shouldAnimate && 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; cancelRealtime(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; cancelRealtime(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. */ 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 sprintf("%02d:%02d:%02d.%03d", gregorianDate.hour, gregorianDate.minute, gregorianDate.second, millisecond); } return sprintf("%02d:%02d:%02d UTC", gregorianDate.hour, gregorianDate.minute, gregorianDate.second); }; /** * Gets a copy of the array of positive known clock multipliers to associate with the shuttle ring. * * @returns 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; }; 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. */ return AnimationViewModel; });