Timeline.js 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012
  1. import {
  2. ClockRange,
  3. defined,
  4. destroyObject,
  5. DeveloperError,
  6. getElement,
  7. JulianDate,
  8. } from "@cesium/engine";
  9. import TimelineHighlightRange from "./TimelineHighlightRange.js";
  10. import TimelineTrack from "./TimelineTrack.js";
  11. let timelineWheelDelta = 1e12;
  12. const timelineMouseMode = {
  13. none: 0,
  14. scrub: 1,
  15. slide: 2,
  16. zoom: 3,
  17. touchOnly: 4,
  18. };
  19. const timelineTouchMode = {
  20. none: 0,
  21. scrub: 1,
  22. slideZoom: 2,
  23. singleTap: 3,
  24. ignore: 4,
  25. };
  26. const timelineTicScales = [
  27. 0.001,
  28. 0.002,
  29. 0.005,
  30. 0.01,
  31. 0.02,
  32. 0.05,
  33. 0.1,
  34. 0.25,
  35. 0.5,
  36. 1.0,
  37. 2.0,
  38. 5.0,
  39. 10.0,
  40. 15.0,
  41. 30.0,
  42. 60.0, // 1min
  43. 120.0, // 2min
  44. 300.0, // 5min
  45. 600.0, // 10min
  46. 900.0, // 15min
  47. 1800.0, // 30min
  48. 3600.0, // 1hr
  49. 7200.0, // 2hr
  50. 14400.0, // 4hr
  51. 21600.0, // 6hr
  52. 43200.0, // 12hr
  53. 86400.0, // 24hr
  54. 172800.0, // 2days
  55. 345600.0, // 4days
  56. 604800.0, // 7days
  57. 1296000.0, // 15days
  58. 2592000.0, // 30days
  59. 5184000.0, // 60days
  60. 7776000.0, // 90days
  61. 15552000.0, // 180days
  62. 31536000.0, // 365days
  63. 63072000.0, // 2years
  64. 126144000.0, // 4years
  65. 157680000.0, // 5years
  66. 315360000.0, // 10years
  67. 630720000.0, // 20years
  68. 1261440000.0, // 40years
  69. 1576800000.0, // 50years
  70. 3153600000.0, // 100years
  71. 6307200000.0, // 200years
  72. 12614400000.0, // 400years
  73. 15768000000.0, // 500years
  74. 31536000000.0, // 1000years
  75. ];
  76. const timelineMonthNames = [
  77. "Jan",
  78. "Feb",
  79. "Mar",
  80. "Apr",
  81. "May",
  82. "Jun",
  83. "Jul",
  84. "Aug",
  85. "Sep",
  86. "Oct",
  87. "Nov",
  88. "Dec",
  89. ];
  90. /**
  91. * The Timeline is a widget for displaying and controlling the current scene time.
  92. * @alias Timeline
  93. * @constructor
  94. *
  95. * @param {Element} container The parent HTML container node for this widget.
  96. * @param {Clock} clock The clock to use.
  97. */
  98. function Timeline(container, clock) {
  99. //>>includeStart('debug', pragmas.debug);
  100. if (!defined(container)) {
  101. throw new DeveloperError("container is required.");
  102. }
  103. if (!defined(clock)) {
  104. throw new DeveloperError("clock is required.");
  105. }
  106. //>>includeEnd('debug');
  107. container = getElement(container);
  108. const ownerDocument = container.ownerDocument;
  109. /**
  110. * Gets the parent container.
  111. * @type {Element}
  112. */
  113. this.container = container;
  114. const topDiv = ownerDocument.createElement("div");
  115. topDiv.className = "cesium-timeline-main";
  116. container.appendChild(topDiv);
  117. this._topDiv = topDiv;
  118. this._endJulian = undefined;
  119. this._epochJulian = undefined;
  120. this._lastXPos = undefined;
  121. this._scrubElement = undefined;
  122. this._startJulian = undefined;
  123. this._timeBarSecondsSpan = undefined;
  124. this._clock = clock;
  125. this._scrubJulian = clock.currentTime;
  126. this._mainTicSpan = -1;
  127. this._mouseMode = timelineMouseMode.none;
  128. this._touchMode = timelineTouchMode.none;
  129. this._touchState = {
  130. centerX: 0,
  131. spanX: 0,
  132. };
  133. this._mouseX = 0;
  134. this._timelineDrag = 0;
  135. this._timelineDragLocation = undefined;
  136. this._lastHeight = undefined;
  137. this._lastWidth = undefined;
  138. this._topDiv.innerHTML =
  139. '<div class="cesium-timeline-bar"></div><div class="cesium-timeline-trackContainer">' +
  140. '<canvas class="cesium-timeline-tracks" width="10" height="1">' +
  141. '</canvas></div><div class="cesium-timeline-needle"></div><span class="cesium-timeline-ruler"></span>';
  142. this._timeBarEle = this._topDiv.childNodes[0];
  143. this._trackContainer = this._topDiv.childNodes[1];
  144. this._trackListEle = this._topDiv.childNodes[1].childNodes[0];
  145. this._needleEle = this._topDiv.childNodes[2];
  146. this._rulerEle = this._topDiv.childNodes[3];
  147. this._context = this._trackListEle.getContext("2d");
  148. this._trackList = [];
  149. this._highlightRanges = [];
  150. this.zoomTo(clock.startTime, clock.stopTime);
  151. this._onMouseDown = createMouseDownCallback(this);
  152. this._onMouseUp = createMouseUpCallback(this);
  153. this._onMouseMove = createMouseMoveCallback(this);
  154. this._onMouseWheel = createMouseWheelCallback(this);
  155. this._onTouchStart = createTouchStartCallback(this);
  156. this._onTouchMove = createTouchMoveCallback(this);
  157. this._onTouchEnd = createTouchEndCallback(this);
  158. const timeBarEle = this._timeBarEle;
  159. ownerDocument.addEventListener("mouseup", this._onMouseUp, false);
  160. ownerDocument.addEventListener("mousemove", this._onMouseMove, false);
  161. timeBarEle.addEventListener("mousedown", this._onMouseDown, false);
  162. timeBarEle.addEventListener("DOMMouseScroll", this._onMouseWheel, false); // Mozilla mouse wheel
  163. timeBarEle.addEventListener("mousewheel", this._onMouseWheel, false);
  164. timeBarEle.addEventListener("touchstart", this._onTouchStart, false);
  165. timeBarEle.addEventListener("touchmove", this._onTouchMove, false);
  166. timeBarEle.addEventListener("touchend", this._onTouchEnd, false);
  167. timeBarEle.addEventListener("touchcancel", this._onTouchEnd, false);
  168. this._topDiv.oncontextmenu = function () {
  169. return false;
  170. };
  171. clock.onTick.addEventListener(this.updateFromClock, this);
  172. this.updateFromClock();
  173. }
  174. /**
  175. * @private
  176. */
  177. Timeline.prototype.addEventListener = function (type, listener, useCapture) {
  178. this._topDiv.addEventListener(type, listener, useCapture);
  179. };
  180. /**
  181. * @private
  182. */
  183. Timeline.prototype.removeEventListener = function (type, listener, useCapture) {
  184. this._topDiv.removeEventListener(type, listener, useCapture);
  185. };
  186. /**
  187. * @returns {boolean} true if the object has been destroyed, false otherwise.
  188. */
  189. Timeline.prototype.isDestroyed = function () {
  190. return false;
  191. };
  192. /**
  193. * Destroys the widget. Should be called if permanently
  194. * removing the widget from layout.
  195. */
  196. Timeline.prototype.destroy = function () {
  197. this._clock.onTick.removeEventListener(this.updateFromClock, this);
  198. const doc = this.container.ownerDocument;
  199. doc.removeEventListener("mouseup", this._onMouseUp, false);
  200. doc.removeEventListener("mousemove", this._onMouseMove, false);
  201. const timeBarEle = this._timeBarEle;
  202. timeBarEle.removeEventListener("mousedown", this._onMouseDown, false);
  203. timeBarEle.removeEventListener("DOMMouseScroll", this._onMouseWheel, false); // Mozilla mouse wheel
  204. timeBarEle.removeEventListener("mousewheel", this._onMouseWheel, false);
  205. timeBarEle.removeEventListener("touchstart", this._onTouchStart, false);
  206. timeBarEle.removeEventListener("touchmove", this._onTouchMove, false);
  207. timeBarEle.removeEventListener("touchend", this._onTouchEnd, false);
  208. timeBarEle.removeEventListener("touchcancel", this._onTouchEnd, false);
  209. this.container.removeChild(this._topDiv);
  210. destroyObject(this);
  211. };
  212. /**
  213. * @private
  214. */
  215. Timeline.prototype.addHighlightRange = function (color, heightInPx, base) {
  216. const newHighlightRange = new TimelineHighlightRange(color, heightInPx, base);
  217. this._highlightRanges.push(newHighlightRange);
  218. this.resize();
  219. return newHighlightRange;
  220. };
  221. /**
  222. * @private
  223. */
  224. Timeline.prototype.addTrack = function (
  225. interval,
  226. heightInPx,
  227. color,
  228. backgroundColor
  229. ) {
  230. const newTrack = new TimelineTrack(
  231. interval,
  232. heightInPx,
  233. color,
  234. backgroundColor
  235. );
  236. this._trackList.push(newTrack);
  237. this._lastHeight = undefined;
  238. this.resize();
  239. return newTrack;
  240. };
  241. /**
  242. * Sets the view to the provided times.
  243. *
  244. * @param {JulianDate} startTime The start time.
  245. * @param {JulianDate} stopTime The stop time.
  246. */
  247. Timeline.prototype.zoomTo = function (startTime, stopTime) {
  248. //>>includeStart('debug', pragmas.debug);
  249. if (!defined(startTime)) {
  250. throw new DeveloperError("startTime is required.");
  251. }
  252. if (!defined(stopTime)) {
  253. throw new DeveloperError("stopTime is required");
  254. }
  255. if (JulianDate.lessThanOrEquals(stopTime, startTime)) {
  256. throw new DeveloperError("Start time must come before end time.");
  257. }
  258. //>>includeEnd('debug');
  259. this._startJulian = startTime;
  260. this._endJulian = stopTime;
  261. this._timeBarSecondsSpan = JulianDate.secondsDifference(stopTime, startTime);
  262. // If clock is not unbounded, clamp timeline range to clock.
  263. if (this._clock && this._clock.clockRange !== ClockRange.UNBOUNDED) {
  264. const clockStart = this._clock.startTime;
  265. const clockEnd = this._clock.stopTime;
  266. const clockSpan = JulianDate.secondsDifference(clockEnd, clockStart);
  267. const startOffset = JulianDate.secondsDifference(
  268. clockStart,
  269. this._startJulian
  270. );
  271. const endOffset = JulianDate.secondsDifference(clockEnd, this._endJulian);
  272. if (this._timeBarSecondsSpan >= clockSpan) {
  273. // if new duration longer than clock range duration, clamp to full range.
  274. this._timeBarSecondsSpan = clockSpan;
  275. this._startJulian = this._clock.startTime;
  276. this._endJulian = this._clock.stopTime;
  277. } else if (startOffset > 0) {
  278. // if timeline start is before clock start, shift right
  279. this._endJulian = JulianDate.addSeconds(
  280. this._endJulian,
  281. startOffset,
  282. new JulianDate()
  283. );
  284. this._startJulian = clockStart;
  285. this._timeBarSecondsSpan = JulianDate.secondsDifference(
  286. this._endJulian,
  287. this._startJulian
  288. );
  289. } else if (endOffset < 0) {
  290. // if timeline end is after clock end, shift left
  291. this._startJulian = JulianDate.addSeconds(
  292. this._startJulian,
  293. endOffset,
  294. new JulianDate()
  295. );
  296. this._endJulian = clockEnd;
  297. this._timeBarSecondsSpan = JulianDate.secondsDifference(
  298. this._endJulian,
  299. this._startJulian
  300. );
  301. }
  302. }
  303. this._makeTics();
  304. const evt = document.createEvent("Event");
  305. evt.initEvent("setzoom", true, true);
  306. evt.startJulian = this._startJulian;
  307. evt.endJulian = this._endJulian;
  308. evt.epochJulian = this._epochJulian;
  309. evt.totalSpan = this._timeBarSecondsSpan;
  310. evt.mainTicSpan = this._mainTicSpan;
  311. this._topDiv.dispatchEvent(evt);
  312. };
  313. /**
  314. * @private
  315. */
  316. Timeline.prototype.zoomFrom = function (amount) {
  317. let centerSec = JulianDate.secondsDifference(
  318. this._scrubJulian,
  319. this._startJulian
  320. );
  321. if (amount > 1 || centerSec < 0 || centerSec > this._timeBarSecondsSpan) {
  322. centerSec = this._timeBarSecondsSpan * 0.5;
  323. } else {
  324. centerSec += centerSec - this._timeBarSecondsSpan * 0.5;
  325. }
  326. const centerSecFlip = this._timeBarSecondsSpan - centerSec;
  327. this.zoomTo(
  328. JulianDate.addSeconds(
  329. this._startJulian,
  330. centerSec - centerSec * amount,
  331. new JulianDate()
  332. ),
  333. JulianDate.addSeconds(
  334. this._endJulian,
  335. centerSecFlip * amount - centerSecFlip,
  336. new JulianDate()
  337. )
  338. );
  339. };
  340. function twoDigits(num) {
  341. return num < 10 ? `0${num.toString()}` : num.toString();
  342. }
  343. /**
  344. * @private
  345. */
  346. Timeline.prototype.makeLabel = function (time) {
  347. const gregorian = JulianDate.toGregorianDate(time);
  348. const millisecond = gregorian.millisecond;
  349. let millisecondString = " UTC";
  350. if (millisecond > 0 && this._timeBarSecondsSpan < 3600) {
  351. millisecondString = Math.floor(millisecond).toString();
  352. while (millisecondString.length < 3) {
  353. millisecondString = `0${millisecondString}`;
  354. }
  355. millisecondString = `.${millisecondString}`;
  356. }
  357. return `${timelineMonthNames[gregorian.month - 1]} ${gregorian.day} ${
  358. gregorian.year
  359. } ${twoDigits(gregorian.hour)}:${twoDigits(gregorian.minute)}:${twoDigits(
  360. gregorian.second
  361. )}${millisecondString}`;
  362. };
  363. /**
  364. * @private
  365. */
  366. Timeline.prototype.smallestTicInPixels = 7.0;
  367. /**
  368. * @private
  369. */
  370. Timeline.prototype._makeTics = function () {
  371. const timeBar = this._timeBarEle;
  372. const seconds = JulianDate.secondsDifference(
  373. this._scrubJulian,
  374. this._startJulian
  375. );
  376. const xPos = Math.round(
  377. (seconds * this._topDiv.clientWidth) / this._timeBarSecondsSpan
  378. );
  379. const scrubX = xPos - 8;
  380. let tic;
  381. const widget = this;
  382. this._needleEle.style.left = `${xPos.toString()}px`;
  383. let tics = "";
  384. const minimumDuration = 0.01;
  385. const maximumDuration = 31536000000.0; // ~1000 years
  386. const epsilon = 1e-10;
  387. // If time step size is known, enter it here...
  388. let minSize = 0;
  389. let duration = this._timeBarSecondsSpan;
  390. if (duration < minimumDuration) {
  391. duration = minimumDuration;
  392. this._timeBarSecondsSpan = minimumDuration;
  393. this._endJulian = JulianDate.addSeconds(
  394. this._startJulian,
  395. minimumDuration,
  396. new JulianDate()
  397. );
  398. } else if (duration > maximumDuration) {
  399. duration = maximumDuration;
  400. this._timeBarSecondsSpan = maximumDuration;
  401. this._endJulian = JulianDate.addSeconds(
  402. this._startJulian,
  403. maximumDuration,
  404. new JulianDate()
  405. );
  406. }
  407. let timeBarWidth = this._timeBarEle.clientWidth;
  408. if (timeBarWidth < 10) {
  409. timeBarWidth = 10;
  410. }
  411. const startJulian = this._startJulian;
  412. // epsilonTime: a small fraction of one pixel width of the timeline, measured in seconds.
  413. const epsilonTime = Math.min((duration / timeBarWidth) * 1e-5, 0.4);
  414. // epochJulian: a nearby time to be considered "zero seconds", should be a round-ish number by human standards.
  415. let epochJulian;
  416. const gregorianDate = JulianDate.toGregorianDate(startJulian);
  417. if (duration > 315360000) {
  418. // 3650+ days visible, epoch is start of the first visible century.
  419. epochJulian = JulianDate.fromDate(
  420. new Date(Date.UTC(Math.floor(gregorianDate.year / 100) * 100, 0))
  421. );
  422. } else if (duration > 31536000) {
  423. // 365+ days visible, epoch is start of the first visible decade.
  424. epochJulian = JulianDate.fromDate(
  425. new Date(Date.UTC(Math.floor(gregorianDate.year / 10) * 10, 0))
  426. );
  427. } else if (duration > 86400) {
  428. // 1+ day(s) visible, epoch is start of the year.
  429. epochJulian = JulianDate.fromDate(
  430. new Date(Date.UTC(gregorianDate.year, 0))
  431. );
  432. } else {
  433. // Less than a day on timeline, epoch is midnight of the visible day.
  434. epochJulian = JulianDate.fromDate(
  435. new Date(
  436. Date.UTC(gregorianDate.year, gregorianDate.month, gregorianDate.day)
  437. )
  438. );
  439. }
  440. // startTime: Seconds offset of the left side of the timeline from epochJulian.
  441. const startTime = JulianDate.secondsDifference(
  442. this._startJulian,
  443. JulianDate.addSeconds(epochJulian, epsilonTime, new JulianDate())
  444. );
  445. // endTime: Seconds offset of the right side of the timeline from epochJulian.
  446. let endTime = startTime + duration;
  447. this._epochJulian = epochJulian;
  448. function getStartTic(ticScale) {
  449. return Math.floor(startTime / ticScale) * ticScale;
  450. }
  451. function getNextTic(tic, ticScale) {
  452. return Math.ceil(tic / ticScale + 0.5) * ticScale;
  453. }
  454. function getAlpha(time) {
  455. return (time - startTime) / duration;
  456. }
  457. function remainder(x, y) {
  458. //return x % y;
  459. return x - y * Math.round(x / y);
  460. }
  461. // Width in pixels of a typical label, plus padding
  462. this._rulerEle.innerHTML = this.makeLabel(
  463. JulianDate.addSeconds(this._endJulian, -minimumDuration, new JulianDate())
  464. );
  465. let sampleWidth = this._rulerEle.offsetWidth + 20;
  466. if (sampleWidth < 30) {
  467. // Workaround an apparent IE bug with measuring the width after going full-screen from inside an iframe.
  468. sampleWidth = 180;
  469. }
  470. const origMinSize = minSize;
  471. minSize -= epsilon;
  472. const renderState = {
  473. startTime: startTime,
  474. startJulian: startJulian,
  475. epochJulian: epochJulian,
  476. duration: duration,
  477. timeBarWidth: timeBarWidth,
  478. getAlpha: getAlpha,
  479. };
  480. this._highlightRanges.forEach(function (highlightRange) {
  481. tics += highlightRange.render(renderState);
  482. });
  483. // Calculate tic mark label spacing in the TimeBar.
  484. let mainTic = 0.0,
  485. subTic = 0.0,
  486. tinyTic = 0.0;
  487. // Ideal labeled tic as percentage of zoom interval
  488. let idealTic = sampleWidth / timeBarWidth;
  489. if (idealTic > 1.0) {
  490. // Clamp to width of window, for thin windows.
  491. idealTic = 1.0;
  492. }
  493. // Ideal labeled tic size in seconds
  494. idealTic *= this._timeBarSecondsSpan;
  495. let ticIndex = -1,
  496. smallestIndex = -1;
  497. const ticScaleLen = timelineTicScales.length;
  498. let i;
  499. for (i = 0; i < ticScaleLen; ++i) {
  500. const sc = timelineTicScales[i];
  501. ++ticIndex;
  502. mainTic = sc;
  503. // Find acceptable main tic size not smaller than ideal size.
  504. if (sc > idealTic && sc > minSize) {
  505. break;
  506. }
  507. if (
  508. smallestIndex < 0 &&
  509. timeBarWidth * (sc / this._timeBarSecondsSpan) >= this.smallestTicInPixels
  510. ) {
  511. smallestIndex = ticIndex;
  512. }
  513. }
  514. if (ticIndex > 0) {
  515. while (ticIndex > 0) {
  516. // Compute sub-tic size that evenly divides main tic.
  517. --ticIndex;
  518. if (Math.abs(remainder(mainTic, timelineTicScales[ticIndex])) < 0.00001) {
  519. if (timelineTicScales[ticIndex] >= minSize) {
  520. subTic = timelineTicScales[ticIndex];
  521. }
  522. break;
  523. }
  524. }
  525. if (smallestIndex >= 0) {
  526. while (smallestIndex < ticIndex) {
  527. // Compute tiny tic size that evenly divides sub-tic.
  528. if (
  529. Math.abs(remainder(subTic, timelineTicScales[smallestIndex])) <
  530. 0.00001 &&
  531. timelineTicScales[smallestIndex] >= minSize
  532. ) {
  533. tinyTic = timelineTicScales[smallestIndex];
  534. break;
  535. }
  536. ++smallestIndex;
  537. }
  538. }
  539. }
  540. minSize = origMinSize;
  541. if (
  542. minSize > epsilon &&
  543. tinyTic < 0.00001 &&
  544. Math.abs(minSize - mainTic) > epsilon
  545. ) {
  546. tinyTic = minSize;
  547. if (minSize <= mainTic + epsilon) {
  548. subTic = 0.0;
  549. }
  550. }
  551. let lastTextLeft = -999999,
  552. textWidth;
  553. if (timeBarWidth * (tinyTic / this._timeBarSecondsSpan) >= 3.0) {
  554. for (
  555. tic = getStartTic(tinyTic);
  556. tic <= endTime;
  557. tic = getNextTic(tic, tinyTic)
  558. ) {
  559. tics += `<span class="cesium-timeline-ticTiny" style="left: ${Math.round(
  560. timeBarWidth * getAlpha(tic)
  561. ).toString()}px;"></span>`;
  562. }
  563. }
  564. if (timeBarWidth * (subTic / this._timeBarSecondsSpan) >= 3.0) {
  565. for (
  566. tic = getStartTic(subTic);
  567. tic <= endTime;
  568. tic = getNextTic(tic, subTic)
  569. ) {
  570. tics += `<span class="cesium-timeline-ticSub" style="left: ${Math.round(
  571. timeBarWidth * getAlpha(tic)
  572. ).toString()}px;"></span>`;
  573. }
  574. }
  575. if (timeBarWidth * (mainTic / this._timeBarSecondsSpan) >= 2.0) {
  576. this._mainTicSpan = mainTic;
  577. endTime += mainTic;
  578. tic = getStartTic(mainTic);
  579. const leapSecond = JulianDate.computeTaiMinusUtc(epochJulian);
  580. while (tic <= endTime) {
  581. let ticTime = JulianDate.addSeconds(
  582. startJulian,
  583. tic - startTime,
  584. new JulianDate()
  585. );
  586. if (mainTic > 2.1) {
  587. const ticLeap = JulianDate.computeTaiMinusUtc(ticTime);
  588. if (Math.abs(ticLeap - leapSecond) > 0.1) {
  589. tic += ticLeap - leapSecond;
  590. ticTime = JulianDate.addSeconds(
  591. startJulian,
  592. tic - startTime,
  593. new JulianDate()
  594. );
  595. }
  596. }
  597. const ticLeft = Math.round(timeBarWidth * getAlpha(tic));
  598. const ticLabel = this.makeLabel(ticTime);
  599. this._rulerEle.innerHTML = ticLabel;
  600. textWidth = this._rulerEle.offsetWidth;
  601. if (textWidth < 10) {
  602. // IE iframe fullscreen sampleWidth workaround, continued.
  603. textWidth = sampleWidth;
  604. }
  605. const labelLeft = ticLeft - (textWidth / 2 - 1);
  606. if (labelLeft > lastTextLeft) {
  607. lastTextLeft = labelLeft + textWidth + 5;
  608. tics +=
  609. `<span class="cesium-timeline-ticMain" style="left: ${ticLeft.toString()}px;"></span>` +
  610. `<span class="cesium-timeline-ticLabel" style="left: ${labelLeft.toString()}px;">${ticLabel}</span>`;
  611. } else {
  612. tics += `<span class="cesium-timeline-ticSub" style="left: ${ticLeft.toString()}px;"></span>`;
  613. }
  614. tic = getNextTic(tic, mainTic);
  615. }
  616. } else {
  617. this._mainTicSpan = -1;
  618. }
  619. tics += `<span class="cesium-timeline-icon16" style="left:${scrubX}px;bottom:0;background-position: 0 0;"></span>`;
  620. timeBar.innerHTML = tics;
  621. this._scrubElement = timeBar.lastChild;
  622. // Clear track canvas.
  623. this._context.clearRect(
  624. 0,
  625. 0,
  626. this._trackListEle.width,
  627. this._trackListEle.height
  628. );
  629. renderState.y = 0;
  630. this._trackList.forEach(function (track) {
  631. track.render(widget._context, renderState);
  632. renderState.y += track.height;
  633. });
  634. };
  635. /**
  636. * @private
  637. */
  638. Timeline.prototype.updateFromClock = function () {
  639. this._scrubJulian = this._clock.currentTime;
  640. const scrubElement = this._scrubElement;
  641. if (defined(this._scrubElement)) {
  642. const seconds = JulianDate.secondsDifference(
  643. this._scrubJulian,
  644. this._startJulian
  645. );
  646. const xPos = Math.round(
  647. (seconds * this._topDiv.clientWidth) / this._timeBarSecondsSpan
  648. );
  649. if (this._lastXPos !== xPos) {
  650. this._lastXPos = xPos;
  651. scrubElement.style.left = `${xPos - 8}px`;
  652. this._needleEle.style.left = `${xPos}px`;
  653. }
  654. }
  655. if (defined(this._timelineDragLocation)) {
  656. this._setTimeBarTime(
  657. this._timelineDragLocation,
  658. (this._timelineDragLocation * this._timeBarSecondsSpan) /
  659. this._topDiv.clientWidth
  660. );
  661. this.zoomTo(
  662. JulianDate.addSeconds(
  663. this._startJulian,
  664. this._timelineDrag,
  665. new JulianDate()
  666. ),
  667. JulianDate.addSeconds(
  668. this._endJulian,
  669. this._timelineDrag,
  670. new JulianDate()
  671. )
  672. );
  673. }
  674. };
  675. /**
  676. * @private
  677. */
  678. Timeline.prototype._setTimeBarTime = function (xPos, seconds) {
  679. xPos = Math.round(xPos);
  680. this._scrubJulian = JulianDate.addSeconds(
  681. this._startJulian,
  682. seconds,
  683. new JulianDate()
  684. );
  685. if (this._scrubElement) {
  686. const scrubX = xPos - 8;
  687. this._scrubElement.style.left = `${scrubX.toString()}px`;
  688. this._needleEle.style.left = `${xPos.toString()}px`;
  689. }
  690. const evt = document.createEvent("Event");
  691. evt.initEvent("settime", true, true);
  692. evt.clientX = xPos;
  693. evt.timeSeconds = seconds;
  694. evt.timeJulian = this._scrubJulian;
  695. evt.clock = this._clock;
  696. this._topDiv.dispatchEvent(evt);
  697. };
  698. function createMouseDownCallback(timeline) {
  699. return function (e) {
  700. if (timeline._mouseMode !== timelineMouseMode.touchOnly) {
  701. if (e.button === 0) {
  702. timeline._mouseMode = timelineMouseMode.scrub;
  703. if (timeline._scrubElement) {
  704. timeline._scrubElement.style.backgroundPosition = "-16px 0";
  705. }
  706. timeline._onMouseMove(e);
  707. } else {
  708. timeline._mouseX = e.clientX;
  709. if (e.button === 2) {
  710. timeline._mouseMode = timelineMouseMode.zoom;
  711. } else {
  712. timeline._mouseMode = timelineMouseMode.slide;
  713. }
  714. }
  715. }
  716. e.preventDefault();
  717. };
  718. }
  719. function createMouseUpCallback(timeline) {
  720. return function (e) {
  721. timeline._mouseMode = timelineMouseMode.none;
  722. if (timeline._scrubElement) {
  723. timeline._scrubElement.style.backgroundPosition = "0 0";
  724. }
  725. timeline._timelineDrag = 0;
  726. timeline._timelineDragLocation = undefined;
  727. };
  728. }
  729. function createMouseMoveCallback(timeline) {
  730. return function (e) {
  731. let dx;
  732. if (timeline._mouseMode === timelineMouseMode.scrub) {
  733. e.preventDefault();
  734. const x = e.clientX - timeline._topDiv.getBoundingClientRect().left;
  735. if (x < 0) {
  736. timeline._timelineDragLocation = 0;
  737. timeline._timelineDrag = -0.01 * timeline._timeBarSecondsSpan;
  738. } else if (x > timeline._topDiv.clientWidth) {
  739. timeline._timelineDragLocation = timeline._topDiv.clientWidth;
  740. timeline._timelineDrag = 0.01 * timeline._timeBarSecondsSpan;
  741. } else {
  742. timeline._timelineDragLocation = undefined;
  743. timeline._setTimeBarTime(
  744. x,
  745. (x * timeline._timeBarSecondsSpan) / timeline._topDiv.clientWidth
  746. );
  747. }
  748. } else if (timeline._mouseMode === timelineMouseMode.slide) {
  749. dx = timeline._mouseX - e.clientX;
  750. timeline._mouseX = e.clientX;
  751. if (dx !== 0) {
  752. const dsec =
  753. (dx * timeline._timeBarSecondsSpan) / timeline._topDiv.clientWidth;
  754. timeline.zoomTo(
  755. JulianDate.addSeconds(timeline._startJulian, dsec, new JulianDate()),
  756. JulianDate.addSeconds(timeline._endJulian, dsec, new JulianDate())
  757. );
  758. }
  759. } else if (timeline._mouseMode === timelineMouseMode.zoom) {
  760. dx = timeline._mouseX - e.clientX;
  761. timeline._mouseX = e.clientX;
  762. if (dx !== 0) {
  763. timeline.zoomFrom(Math.pow(1.01, dx));
  764. }
  765. }
  766. };
  767. }
  768. function createMouseWheelCallback(timeline) {
  769. return function (e) {
  770. let dy = e.wheelDeltaY || e.wheelDelta || -e.detail;
  771. timelineWheelDelta = Math.max(
  772. Math.min(Math.abs(dy), timelineWheelDelta),
  773. 1
  774. );
  775. dy /= timelineWheelDelta;
  776. timeline.zoomFrom(Math.pow(1.05, -dy));
  777. };
  778. }
  779. function createTouchStartCallback(timeline) {
  780. return function (e) {
  781. const len = e.touches.length;
  782. let seconds, xPos;
  783. const leftX = timeline._topDiv.getBoundingClientRect().left;
  784. e.preventDefault();
  785. timeline._mouseMode = timelineMouseMode.touchOnly;
  786. if (len === 1) {
  787. seconds = JulianDate.secondsDifference(
  788. timeline._scrubJulian,
  789. timeline._startJulian
  790. );
  791. xPos = Math.round(
  792. (seconds * timeline._topDiv.clientWidth) /
  793. timeline._timeBarSecondsSpan +
  794. leftX
  795. );
  796. if (Math.abs(e.touches[0].clientX - xPos) < 50) {
  797. timeline._touchMode = timelineTouchMode.scrub;
  798. if (timeline._scrubElement) {
  799. timeline._scrubElement.style.backgroundPosition =
  800. len === 1 ? "-16px 0" : "0 0";
  801. }
  802. } else {
  803. timeline._touchMode = timelineTouchMode.singleTap;
  804. timeline._touchState.centerX = e.touches[0].clientX - leftX;
  805. }
  806. } else if (len === 2) {
  807. timeline._touchMode = timelineTouchMode.slideZoom;
  808. timeline._touchState.centerX =
  809. (e.touches[0].clientX + e.touches[1].clientX) * 0.5 - leftX;
  810. timeline._touchState.spanX = Math.abs(
  811. e.touches[0].clientX - e.touches[1].clientX
  812. );
  813. } else {
  814. timeline._touchMode = timelineTouchMode.ignore;
  815. }
  816. };
  817. }
  818. function createTouchEndCallback(timeline) {
  819. return function (e) {
  820. const len = e.touches.length,
  821. leftX = timeline._topDiv.getBoundingClientRect().left;
  822. if (timeline._touchMode === timelineTouchMode.singleTap) {
  823. timeline._touchMode = timelineTouchMode.scrub;
  824. timeline._onTouchMove(e);
  825. } else if (timeline._touchMode === timelineTouchMode.scrub) {
  826. timeline._onTouchMove(e);
  827. }
  828. timeline._mouseMode = timelineMouseMode.touchOnly;
  829. if (len !== 1) {
  830. timeline._touchMode =
  831. len > 0 ? timelineTouchMode.ignore : timelineTouchMode.none;
  832. } else if (timeline._touchMode === timelineTouchMode.slideZoom) {
  833. timeline._touchState.centerX = e.touches[0].clientX - leftX;
  834. }
  835. if (timeline._scrubElement) {
  836. timeline._scrubElement.style.backgroundPosition = "0 0";
  837. }
  838. };
  839. }
  840. function createTouchMoveCallback(timeline) {
  841. return function (e) {
  842. let dx,
  843. x,
  844. len,
  845. newCenter,
  846. newSpan,
  847. newStartTime,
  848. zoom = 1;
  849. const leftX = timeline._topDiv.getBoundingClientRect().left;
  850. if (timeline._touchMode === timelineTouchMode.singleTap) {
  851. timeline._touchMode = timelineTouchMode.slideZoom;
  852. }
  853. timeline._mouseMode = timelineMouseMode.touchOnly;
  854. if (timeline._touchMode === timelineTouchMode.scrub) {
  855. e.preventDefault();
  856. if (e.changedTouches.length === 1) {
  857. x = e.changedTouches[0].clientX - leftX;
  858. if (x >= 0 && x <= timeline._topDiv.clientWidth) {
  859. timeline._setTimeBarTime(
  860. x,
  861. (x * timeline._timeBarSecondsSpan) / timeline._topDiv.clientWidth
  862. );
  863. }
  864. }
  865. } else if (timeline._touchMode === timelineTouchMode.slideZoom) {
  866. len = e.touches.length;
  867. if (len === 2) {
  868. newCenter = (e.touches[0].clientX + e.touches[1].clientX) * 0.5 - leftX;
  869. newSpan = Math.abs(e.touches[0].clientX - e.touches[1].clientX);
  870. } else if (len === 1) {
  871. newCenter = e.touches[0].clientX - leftX;
  872. newSpan = 0;
  873. }
  874. if (defined(newCenter)) {
  875. if (newSpan > 0 && timeline._touchState.spanX > 0) {
  876. // Zoom and slide
  877. zoom = timeline._touchState.spanX / newSpan;
  878. newStartTime = JulianDate.addSeconds(
  879. timeline._startJulian,
  880. (timeline._touchState.centerX * timeline._timeBarSecondsSpan -
  881. newCenter * timeline._timeBarSecondsSpan * zoom) /
  882. timeline._topDiv.clientWidth,
  883. new JulianDate()
  884. );
  885. } else {
  886. // Slide to newCenter
  887. dx = timeline._touchState.centerX - newCenter;
  888. newStartTime = JulianDate.addSeconds(
  889. timeline._startJulian,
  890. (dx * timeline._timeBarSecondsSpan) / timeline._topDiv.clientWidth,
  891. new JulianDate()
  892. );
  893. }
  894. timeline.zoomTo(
  895. newStartTime,
  896. JulianDate.addSeconds(
  897. newStartTime,
  898. timeline._timeBarSecondsSpan * zoom,
  899. new JulianDate()
  900. )
  901. );
  902. timeline._touchState.centerX = newCenter;
  903. timeline._touchState.spanX = newSpan;
  904. }
  905. }
  906. };
  907. }
  908. /**
  909. * Resizes the widget to match the container size.
  910. */
  911. Timeline.prototype.resize = function () {
  912. const width = this.container.clientWidth;
  913. const height = this.container.clientHeight;
  914. if (width === this._lastWidth && height === this._lastHeight) {
  915. return;
  916. }
  917. this._trackContainer.style.height = `${height}px`;
  918. let trackListHeight = 1;
  919. this._trackList.forEach(function (track) {
  920. trackListHeight += track.height;
  921. });
  922. this._trackListEle.style.height = `${trackListHeight.toString()}px`;
  923. this._trackListEle.width = this._trackListEle.clientWidth;
  924. this._trackListEle.height = trackListHeight;
  925. this._makeTics();
  926. this._lastXPos = undefined;
  927. this._lastWidth = width;
  928. this._lastHeight = height;
  929. };
  930. export default Timeline;