AnimationViewModel.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. /*global define*/
  2. define([
  3. '../../Core/binarySearch',
  4. '../../Core/ClockRange',
  5. '../../Core/ClockStep',
  6. '../../Core/defined',
  7. '../../Core/defineProperties',
  8. '../../Core/DeveloperError',
  9. '../../Core/JulianDate',
  10. '../../ThirdParty/knockout',
  11. '../../ThirdParty/sprintf',
  12. '../createCommand',
  13. '../ToggleButtonViewModel'
  14. ], function(
  15. binarySearch,
  16. ClockRange,
  17. ClockStep,
  18. defined,
  19. defineProperties,
  20. DeveloperError,
  21. JulianDate,
  22. knockout,
  23. sprintf,
  24. createCommand,
  25. ToggleButtonViewModel) {
  26. "use strict";
  27. var monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  28. var realtimeShuttleRingAngle = 15;
  29. var maxShuttleRingAngle = 105;
  30. function cancelRealtime(clockViewModel) {
  31. if (clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK) {
  32. clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
  33. clockViewModel.multiplier = 1;
  34. }
  35. }
  36. function unpause(clockViewModel) {
  37. cancelRealtime(clockViewModel);
  38. clockViewModel.shouldAnimate = true;
  39. }
  40. function numberComparator(left, right) {
  41. return left - right;
  42. }
  43. function getTypicalMultiplierIndex(multiplier, shuttleRingTicks) {
  44. var index = binarySearch(shuttleRingTicks, multiplier, numberComparator);
  45. return index < 0 ? ~index : index;
  46. }
  47. function angleToMultiplier(angle, shuttleRingTicks) {
  48. //Use a linear scale for -1 to 1 between -15 < angle < 15 degrees
  49. if (Math.abs(angle) <= realtimeShuttleRingAngle) {
  50. return angle / realtimeShuttleRingAngle;
  51. }
  52. var minp = realtimeShuttleRingAngle;
  53. var maxp = maxShuttleRingAngle;
  54. var maxv;
  55. var minv = 0;
  56. var scale;
  57. if (angle > 0) {
  58. maxv = Math.log(shuttleRingTicks[shuttleRingTicks.length - 1]);
  59. scale = (maxv - minv) / (maxp - minp);
  60. return Math.exp(minv + scale * (angle - minp));
  61. }
  62. maxv = Math.log(-shuttleRingTicks[0]);
  63. scale = (maxv - minv) / (maxp - minp);
  64. return -Math.exp(minv + scale * (Math.abs(angle) - minp));
  65. }
  66. function multiplierToAngle(multiplier, shuttleRingTicks, clockViewModel) {
  67. if (clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK) {
  68. return realtimeShuttleRingAngle;
  69. }
  70. if (Math.abs(multiplier) <= 1) {
  71. return multiplier * realtimeShuttleRingAngle;
  72. }
  73. var minp = realtimeShuttleRingAngle;
  74. var maxp = maxShuttleRingAngle;
  75. var maxv;
  76. var minv = 0;
  77. var scale;
  78. if (multiplier > 0) {
  79. maxv = Math.log(shuttleRingTicks[shuttleRingTicks.length - 1]);
  80. scale = (maxv - minv) / (maxp - minp);
  81. return (Math.log(multiplier) - minv) / scale + minp;
  82. }
  83. maxv = Math.log(-shuttleRingTicks[0]);
  84. scale = (maxv - minv) / (maxp - minp);
  85. return -((Math.log(Math.abs(multiplier)) - minv) / scale + minp);
  86. }
  87. /**
  88. * The view model for the {@link Animation} widget.
  89. * @alias AnimationViewModel
  90. * @constructor
  91. *
  92. * @param {ClockViewModel} clockViewModel The ClockViewModel instance to use.
  93. *
  94. * @see Animation
  95. */
  96. var AnimationViewModel = function(clockViewModel) {
  97. //>>includeStart('debug', pragmas.debug);
  98. if (!defined(clockViewModel)) {
  99. throw new DeveloperError('clockViewModel is required.');
  100. }
  101. //>>includeEnd('debug');
  102. var that = this;
  103. this._clockViewModel = clockViewModel;
  104. this._allShuttleRingTicks = [];
  105. this._dateFormatter = AnimationViewModel.defaultDateFormatter;
  106. this._timeFormatter = AnimationViewModel.defaultTimeFormatter;
  107. /**
  108. * Gets or sets whether the shuttle ring is currently being dragged. This property is observable.
  109. * @type {Boolean}
  110. * @default false
  111. */
  112. this.shuttleRingDragging = false;
  113. /**
  114. * Gets or sets whether dragging the shuttle ring should cause the multiplier
  115. * to snap to the defined tick values rather than interpolating between them.
  116. * This property is observable.
  117. * @type {Boolean}
  118. * @default false
  119. */
  120. this.snapToTicks = false;
  121. knockout.track(this, ['_allShuttleRingTicks', '_dateFormatter', '_timeFormatter', 'shuttleRingDragging', 'snapToTicks']);
  122. this._sortedFilteredPositiveTicks = [];
  123. this.setShuttleRingTicks(AnimationViewModel.defaultTicks);
  124. /**
  125. * Gets the string representation of the current time. This property is observable.
  126. * @type {String}
  127. */
  128. this.timeLabel = undefined;
  129. knockout.defineProperty(this, 'timeLabel', function() {
  130. return that._timeFormatter(that._clockViewModel.currentTime, that);
  131. });
  132. /**
  133. * Gets the string representation of the current date. This property is observable.
  134. * @type {String}
  135. */
  136. this.dateLabel = undefined;
  137. knockout.defineProperty(this, 'dateLabel', function() {
  138. return that._dateFormatter(that._clockViewModel.currentTime, that);
  139. });
  140. /**
  141. * Gets the string representation of the current multiplier. This property is observable.
  142. * @type {String}
  143. */
  144. this.multiplierLabel = undefined;
  145. knockout.defineProperty(this, 'multiplierLabel', function() {
  146. var clockViewModel = that._clockViewModel;
  147. if (clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK) {
  148. return 'Today';
  149. }
  150. var multiplier = clockViewModel.multiplier;
  151. //If it's a whole number, just return it.
  152. if (multiplier % 1 === 0) {
  153. return multiplier.toFixed(0) + 'x';
  154. }
  155. //Convert to decimal string and remove any trailing zeroes
  156. return multiplier.toFixed(3).replace(/0{0,3}$/, "") + 'x';
  157. });
  158. /**
  159. * Gets or sets the current shuttle ring angle. This property is observable.
  160. * @type {Number}
  161. */
  162. this.shuttleRingAngle = undefined;
  163. knockout.defineProperty(this, 'shuttleRingAngle', {
  164. get : function() {
  165. return multiplierToAngle(clockViewModel.multiplier, that._allShuttleRingTicks, clockViewModel);
  166. },
  167. set : function(angle) {
  168. angle = Math.max(Math.min(angle, maxShuttleRingAngle), -maxShuttleRingAngle);
  169. var ticks = that._allShuttleRingTicks;
  170. var clockViewModel = that._clockViewModel;
  171. clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER;
  172. //If we are at the max angle, simply return the max value in either direction.
  173. if (Math.abs(angle) === maxShuttleRingAngle) {
  174. clockViewModel.multiplier = angle > 0 ? ticks[ticks.length - 1] : ticks[0];
  175. return;
  176. }
  177. var multiplier = angleToMultiplier(angle, ticks);
  178. if (that.snapToTicks) {
  179. multiplier = ticks[getTypicalMultiplierIndex(multiplier, ticks)];
  180. } else {
  181. if (multiplier !== 0) {
  182. var positiveMultiplier = Math.abs(multiplier);
  183. if (positiveMultiplier > 100) {
  184. var numDigits = positiveMultiplier.toFixed(0).length - 2;
  185. var divisor = Math.pow(10, numDigits);
  186. multiplier = (Math.round(multiplier / divisor) * divisor) | 0;
  187. } else if (positiveMultiplier > realtimeShuttleRingAngle) {
  188. multiplier = Math.round(multiplier);
  189. } else if (positiveMultiplier > 1) {
  190. multiplier = +multiplier.toFixed(1);
  191. } else if (positiveMultiplier > 0) {
  192. multiplier = +multiplier.toFixed(2);
  193. }
  194. }
  195. }
  196. clockViewModel.multiplier = multiplier;
  197. }
  198. });
  199. this._canAnimate = undefined;
  200. knockout.defineProperty(this, '_canAnimate', function() {
  201. var clockViewModel = that._clockViewModel;
  202. var clockRange = clockViewModel.clockRange;
  203. if (that.shuttleRingDragging || clockRange === ClockRange.UNBOUNDED) {
  204. return true;
  205. }
  206. var multiplier = clockViewModel.multiplier;
  207. var currentTime = clockViewModel.currentTime;
  208. var startTime = clockViewModel.startTime;
  209. var result = false;
  210. if (clockRange === ClockRange.LOOP_STOP) {
  211. result = JulianDate.greaterThan(currentTime, startTime) || (currentTime.equals(startTime) && multiplier > 0);
  212. } else {
  213. var stopTime = clockViewModel.stopTime;
  214. result = (JulianDate.greaterThan(currentTime, startTime) && JulianDate.lessThan(currentTime, stopTime)) || //
  215. (currentTime.equals(startTime) && multiplier > 0) || //
  216. (currentTime.equals(stopTime) && multiplier < 0);
  217. }
  218. if (!result) {
  219. clockViewModel.shouldAnimate = false;
  220. }
  221. return result;
  222. });
  223. this._isSystemTimeAvailable = undefined;
  224. knockout.defineProperty(this, '_isSystemTimeAvailable', function() {
  225. var clockViewModel = that._clockViewModel;
  226. var clockRange = clockViewModel.clockRange;
  227. if (clockRange === ClockRange.UNBOUNDED) {
  228. return true;
  229. }
  230. var systemTime = clockViewModel.systemTime;
  231. return JulianDate.greaterThanOrEquals(systemTime, clockViewModel.startTime) && JulianDate.lessThanOrEquals(systemTime, clockViewModel.stopTime);
  232. });
  233. this._isAnimating = undefined;
  234. knockout.defineProperty(this, '_isAnimating', function() {
  235. return that._clockViewModel.shouldAnimate && (that._canAnimate || that.shuttleRingDragging);
  236. });
  237. var pauseCommand = createCommand(function() {
  238. var clockViewModel = that._clockViewModel;
  239. if (clockViewModel.shouldAnimate) {
  240. cancelRealtime(clockViewModel);
  241. clockViewModel.shouldAnimate = false;
  242. } else if (that._canAnimate) {
  243. unpause(clockViewModel);
  244. }
  245. });
  246. this._pauseViewModel = new ToggleButtonViewModel(pauseCommand, {
  247. toggled : knockout.computed(function() {
  248. return !that._isAnimating;
  249. }),
  250. tooltip : 'Pause'
  251. });
  252. var playReverseCommand = createCommand(function() {
  253. var clockViewModel = that._clockViewModel;
  254. cancelRealtime(clockViewModel);
  255. var multiplier = clockViewModel.multiplier;
  256. if (multiplier > 0) {
  257. clockViewModel.multiplier = -multiplier;
  258. }
  259. clockViewModel.shouldAnimate = true;
  260. });
  261. this._playReverseViewModel = new ToggleButtonViewModel(playReverseCommand, {
  262. toggled : knockout.computed(function() {
  263. return that._isAnimating && (clockViewModel.multiplier < 0);
  264. }),
  265. tooltip : 'Play Reverse'
  266. });
  267. var playForwardCommand = createCommand(function() {
  268. var clockViewModel = that._clockViewModel;
  269. cancelRealtime(clockViewModel);
  270. var multiplier = clockViewModel.multiplier;
  271. if (multiplier < 0) {
  272. clockViewModel.multiplier = -multiplier;
  273. }
  274. clockViewModel.shouldAnimate = true;
  275. });
  276. this._playForwardViewModel = new ToggleButtonViewModel(playForwardCommand, {
  277. toggled : knockout.computed(function() {
  278. return that._isAnimating && clockViewModel.multiplier > 0 && clockViewModel.clockStep !== ClockStep.SYSTEM_CLOCK;
  279. }),
  280. tooltip : 'Play Forward'
  281. });
  282. var playRealtimeCommand = createCommand(function() {
  283. var clockViewModel = that._clockViewModel;
  284. clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK;
  285. clockViewModel.multiplier = 1.0;
  286. clockViewModel.shouldAnimate = true;
  287. }, knockout.getObservable(this, '_isSystemTimeAvailable'));
  288. this._playRealtimeViewModel = new ToggleButtonViewModel(playRealtimeCommand, {
  289. toggled : knockout.computed(function() {
  290. return clockViewModel.shouldAnimate && clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK;
  291. }),
  292. tooltip : knockout.computed(function() {
  293. return that._isSystemTimeAvailable ? 'Today (real-time)' : 'Current time not in range';
  294. })
  295. });
  296. this._slower = createCommand(function() {
  297. var clockViewModel = that._clockViewModel;
  298. cancelRealtime(clockViewModel);
  299. var shuttleRingTicks = that._allShuttleRingTicks;
  300. var multiplier = clockViewModel.multiplier;
  301. var index = getTypicalMultiplierIndex(multiplier, shuttleRingTicks) - 1;
  302. if (index >= 0) {
  303. clockViewModel.multiplier = shuttleRingTicks[index];
  304. }
  305. });
  306. this._faster = createCommand(function() {
  307. var clockViewModel = that._clockViewModel;
  308. cancelRealtime(clockViewModel);
  309. var shuttleRingTicks = that._allShuttleRingTicks;
  310. var multiplier = clockViewModel.multiplier;
  311. var index = getTypicalMultiplierIndex(multiplier, shuttleRingTicks) + 1;
  312. if (index < shuttleRingTicks.length) {
  313. clockViewModel.multiplier = shuttleRingTicks[index];
  314. }
  315. });
  316. };
  317. /**
  318. * Gets or sets the default date formatter used by new instances.
  319. *
  320. * @member
  321. * @type {AnimationViewModel~DateFormatter}
  322. */
  323. AnimationViewModel.defaultDateFormatter = function(date, viewModel) {
  324. var gregorianDate = JulianDate.toGregorianDate(date);
  325. return monthNames[gregorianDate.month - 1] + ' ' + gregorianDate.day + ' ' + gregorianDate.year;
  326. };
  327. /**
  328. * Gets or sets the default array of known clock multipliers associated with new instances of the shuttle ring.
  329. */
  330. AnimationViewModel.defaultTicks = [//
  331. 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,//
  332. 15.0, 30.0, 60.0, 120.0, 300.0, 600.0, 900.0, 1800.0, 3600.0, 7200.0, 14400.0,//
  333. 21600.0, 43200.0, 86400.0, 172800.0, 345600.0, 604800.0];
  334. /**
  335. * Gets or sets the default time formatter used by new instances.
  336. *
  337. * @member
  338. * @type {AnimationViewModel~TimeFormatter}
  339. */
  340. AnimationViewModel.defaultTimeFormatter = function(date, viewModel) {
  341. var gregorianDate = JulianDate.toGregorianDate(date);
  342. var millisecond = Math.round(gregorianDate.millisecond);
  343. if (Math.abs(viewModel._clockViewModel.multiplier) < 1) {
  344. return sprintf("%02d:%02d:%02d.%03d", gregorianDate.hour, gregorianDate.minute, gregorianDate.second, millisecond);
  345. }
  346. return sprintf("%02d:%02d:%02d UTC", gregorianDate.hour, gregorianDate.minute, gregorianDate.second);
  347. };
  348. /**
  349. * Gets a copy of the array of positive known clock multipliers to associate with the shuttle ring.
  350. *
  351. * @returns The array of known clock multipliers associated with the shuttle ring.
  352. */
  353. AnimationViewModel.prototype.getShuttleRingTicks = function() {
  354. return this._sortedFilteredPositiveTicks.slice(0);
  355. };
  356. /**
  357. * Sets the array of positive known clock multipliers to associate with the shuttle ring.
  358. * These values will have negative equivalents created for them and sets both the minimum
  359. * and maximum range of values for the shuttle ring as well as the values that are snapped
  360. * to when a single click is made. The values need not be in order, as they will be sorted
  361. * automatically, and duplicate values will be removed.
  362. *
  363. * @param {Number[]} positiveTicks The list of known positive clock multipliers to associate with the shuttle ring.
  364. */
  365. AnimationViewModel.prototype.setShuttleRingTicks = function(positiveTicks) {
  366. //>>includeStart('debug', pragmas.debug);
  367. if (!defined(positiveTicks)) {
  368. throw new DeveloperError('positiveTicks is required.');
  369. }
  370. //>>includeEnd('debug');
  371. var i;
  372. var len;
  373. var tick;
  374. var hash = {};
  375. var sortedFilteredPositiveTicks = this._sortedFilteredPositiveTicks;
  376. sortedFilteredPositiveTicks.length = 0;
  377. for (i = 0, len = positiveTicks.length; i < len; ++i) {
  378. tick = positiveTicks[i];
  379. //filter duplicates
  380. if (!hash.hasOwnProperty(tick)) {
  381. hash[tick] = true;
  382. sortedFilteredPositiveTicks.push(tick);
  383. }
  384. }
  385. sortedFilteredPositiveTicks.sort(numberComparator);
  386. var allTicks = [];
  387. for (len = sortedFilteredPositiveTicks.length, i = len - 1; i >= 0; --i) {
  388. tick = sortedFilteredPositiveTicks[i];
  389. if (tick !== 0) {
  390. allTicks.push(-tick);
  391. }
  392. }
  393. Array.prototype.push.apply(allTicks, sortedFilteredPositiveTicks);
  394. this._allShuttleRingTicks = allTicks;
  395. };
  396. defineProperties(AnimationViewModel.prototype, {
  397. /**
  398. * Gets a command that decreases the speed of animation.
  399. * @memberof AnimationViewModel.prototype
  400. * @type {Command}
  401. */
  402. slower : {
  403. get : function() {
  404. return this._slower;
  405. }
  406. },
  407. /**
  408. * Gets a command that increases the speed of animation.
  409. * @memberof AnimationViewModel.prototype
  410. * @type {Command}
  411. */
  412. faster : {
  413. get : function() {
  414. return this._faster;
  415. }
  416. },
  417. /**
  418. * Gets the clock view model.
  419. * @memberof AnimationViewModel.prototype
  420. *
  421. * @type {ClockViewModel}
  422. */
  423. clockViewModel : {
  424. get : function() {
  425. return this._clockViewModel;
  426. }
  427. },
  428. /**
  429. * Gets the pause toggle button view model.
  430. * @memberof AnimationViewModel.prototype
  431. *
  432. * @type {ToggleButtonViewModel}
  433. */
  434. pauseViewModel : {
  435. get : function() {
  436. return this._pauseViewModel;
  437. }
  438. },
  439. /**
  440. * Gets the reverse toggle button view model.
  441. * @memberof AnimationViewModel.prototype
  442. *
  443. * @type {ToggleButtonViewModel}
  444. */
  445. playReverseViewModel : {
  446. get : function() {
  447. return this._playReverseViewModel;
  448. }
  449. },
  450. /**
  451. * Gets the play toggle button view model.
  452. * @memberof AnimationViewModel.prototype
  453. *
  454. * @type {ToggleButtonViewModel}
  455. */
  456. playForwardViewModel : {
  457. get : function() {
  458. return this._playForwardViewModel;
  459. }
  460. },
  461. /**
  462. * Gets the realtime toggle button view model.
  463. * @memberof AnimationViewModel.prototype
  464. *
  465. * @type {ToggleButtonViewModel}
  466. */
  467. playRealtimeViewModel : {
  468. get : function() {
  469. return this._playRealtimeViewModel;
  470. }
  471. },
  472. /**
  473. * Gets or sets the function which formats a date for display.
  474. * @memberof AnimationViewModel.prototype
  475. *
  476. * @type {AnimationViewModel~DateFormatter}
  477. * @default AnimationViewModel.defaultDateFormatter
  478. */
  479. dateFormatter : {
  480. //TODO:@exception {DeveloperError} dateFormatter must be a function.
  481. get : function() {
  482. return this._dateFormatter;
  483. },
  484. set : function(dateFormatter) {
  485. //>>includeStart('debug', pragmas.debug);
  486. if (typeof dateFormatter !== 'function') {
  487. throw new DeveloperError('dateFormatter must be a function');
  488. }
  489. //>>includeEnd('debug');
  490. this._dateFormatter = dateFormatter;
  491. }
  492. },
  493. /**
  494. * Gets or sets the function which formats a time for display.
  495. * @memberof AnimationViewModel.prototype
  496. *
  497. * @type {AnimationViewModel~TimeFormatter}
  498. * @default AnimationViewModel.defaultTimeFormatter
  499. */
  500. timeFormatter : {
  501. //TODO:@exception {DeveloperError} timeFormatter must be a function.
  502. get : function() {
  503. return this._timeFormatter;
  504. },
  505. set : function(timeFormatter) {
  506. //>>includeStart('debug', pragmas.debug);
  507. if (typeof timeFormatter !== 'function') {
  508. throw new DeveloperError('timeFormatter must be a function');
  509. }
  510. //>>includeEnd('debug');
  511. this._timeFormatter = timeFormatter;
  512. }
  513. }
  514. });
  515. //Currently exposed for tests.
  516. AnimationViewModel._maxShuttleRingAngle = maxShuttleRingAngle;
  517. AnimationViewModel._realtimeShuttleRingAngle = realtimeShuttleRingAngle;
  518. /**
  519. * A function that formats a date for display.
  520. * @callback AnimationViewModel~DateFormatter
  521. *
  522. * @param {JulianDate} date The date to be formatted
  523. * @param {AnimationViewModel} viewModel The AnimationViewModel instance requesting formatting.
  524. * @returns {String} The string representation of the calendar date portion of the provided date.
  525. */
  526. /**
  527. * A function that formats a time for display.
  528. * @callback AnimationViewModel~TimeFormatter
  529. *
  530. * @param {JulianDate} date The date to be formatted
  531. * @param {AnimationViewModel} viewModel The AnimationViewModel instance requesting formatting.
  532. * @returns {String} The string representation of the time portion of the provided date.
  533. */
  534. return AnimationViewModel;
  535. });