AnimationViewModel.js 19 KB

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