Timeline.js 29 KB

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