AnimationViewModel.js 21 KB

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