AnimationViewModel.js 19 KB

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