AnimationViewModel.js 19 KB

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