Timeline.js 32 KB

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