TimeQueue.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2019
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. */
  7. define(['baglass/core-client/js/core-client/ui/core/Class', 'underscore'], function (Class, _) {
  8. var TimeQueue = Class.extend({
  9. // this value is also used for the minimum widget duration.
  10. // Issue is that if duration is less than _endBufferTime, the 'show' event
  11. // is removed when we purge the end buckets.
  12. // This causes the widget to not be visible in the end state of the scene.
  13. // the 200 is replicated in DragSlider and TimelineEpisodeEntry
  14. _endBufferTime: 200,
  15. animationDuration: 500,
  16. STATE: {
  17. PLAYING: 'playing',
  18. STOPPED: 'stopped',
  19. PAUSED: 'paused'
  20. },
  21. /**
  22. name is the name of the animation to put in the event. In a perfect workd this would be the same as the timeline name.
  23. entrance should be true if the action will 'show' the widget, false otherwise.
  24. stateGroup is the group to put the event in. Events in different group are able to be applied at the same time.
  25. I.e:
  26. show and hide should be in the same queue since the cancel each other out and can't be 'active' at the same time
  27. pivotIn and highlight should be in different queues since they are 'active' at the same time.
  28. */
  29. _actionToAnimationMap: {
  30. 'show': { name: 'show', entrance: true, stateGroup: 'visibility' },
  31. 'slideInLeft': { name: 'slideInLeft', entrance: true, stateGroup: 'visibility' },
  32. 'slideInRight': { name: 'slideInRight', entrance: true, stateGroup: 'visibility' },
  33. 'slideInTop': { name: 'slideInTop', entrance: true, stateGroup: 'visibility' },
  34. 'slideInBottom': { name: 'slideInBottom', entrance: true, stateGroup: 'visibility' },
  35. 'scaleIn': { name: 'scaleIn', entrance: true, stateGroup: 'visibility' },
  36. 'shrinkIn': { name: 'shrinkIn', entrance: true, stateGroup: 'visibility' },
  37. 'pivotIn': { name: 'pivotIn', entrance: true, stateGroup: 'visibility' },
  38. 'hide': { name: 'hide', entrance: false, stateGroup: 'visibility' },
  39. 'slideOutLeft': { name: 'slideOutLeft', entrance: false, stateGroup: 'visibility' },
  40. 'slideOutRight': { name: 'slideOutRight', entrance: false, stateGroup: 'visibility' },
  41. 'slideOutTop': { name: 'slideOutTop', entrance: false, stateGroup: 'visibility' },
  42. 'slideOutBottom': { name: 'slideOutBottom', entrance: false, stateGroup: 'visibility' },
  43. 'scaleOut': { name: 'scaleOut', entrance: false, stateGroup: 'visibility' },
  44. 'expandOut': { name: 'expandOut', entrance: false, stateGroup: 'visibility' },
  45. 'pivotOut': { name: 'pivotOut', entrance: false, stateGroup: 'visibility' },
  46. 'highlight': { name: 'highlight', entrance: false, stateGroup: 'highlight' },
  47. 'clearHighlight': { name: 'clearHighlight', entrance: false, stateGroup: 'highlight' }
  48. },
  49. init: function init(options) {
  50. TimeQueue.inherited('init', this, arguments);
  51. this.boardModel = options.boardModel;
  52. this.eventRouter = options.eventRouter;
  53. this.currentScene = options.sceneId;
  54. this.widgetHelper = options.widgetHelper;
  55. this._eventQueue = {};
  56. this.currentState = 'stopped';
  57. this.duration = 0; // ms
  58. this.current = 0; // ms
  59. this._tickLength = 50; // ms. [20 fps (1000/50)]
  60. // TODO: investigate making this 100ms. I.e 10 fps.
  61. // _tickLength must match tick value set in StoryPropertiesProvider.js:validateTimerValue()
  62. // TODO: Refactor to better deal with shared constants like this
  63. this._timer = null;
  64. this._startTime = 0; // ms
  65. this._widgetStateCache = {};
  66. this.timeQueue = {
  67. play: this.play.bind(this),
  68. pause: this.pause.bind(this),
  69. stop: this.stop.bind(this),
  70. seek: this.seek.bind(this),
  71. getState: this.getState.bind(this),
  72. setTickLength: this.setTickLength.bind(this),
  73. STATE: this.STATE
  74. };
  75. },
  76. isPlaying: function isPlaying() {
  77. return this.currentState === this.STATE.PLAYING;
  78. },
  79. isStopped: function isStopped() {
  80. return this.currentState === this.STATE.STOPPED;
  81. },
  82. play: function play() {
  83. if (this._timer) {
  84. return false;
  85. }
  86. return this._playScene();
  87. },
  88. isMarker: function isMarker(time) {
  89. if (this._eventQueue[time]) {
  90. return this._eventQueue[time].some(function (event) {
  91. return this._actionToAnimationMap[event.action].entrance || event.action === 'highlight';
  92. }.bind(this));
  93. }
  94. return false;
  95. },
  96. isExitAction: function isExitAction(time) {
  97. if (this._eventQueue[time]) {
  98. return this._eventQueue[time].some(function (event) {
  99. var animationInfo = this._actionToAnimationMap[event.action];
  100. return !animationInfo.entrance && animationInfo.stateGroup === 'visibility';
  101. }.bind(this));
  102. }
  103. return false;
  104. },
  105. jumpToNextMarker: function jumpToNextMarker() {
  106. var nextMarkerTime = this.duration;
  107. var eventTimes = Object.keys(this._eventQueue);
  108. var tickPromises = [];
  109. for (var i = 0; i < eventTimes.length; i++) {
  110. var candidateTime = parseInt(eventTimes[i], 10);
  111. if (candidateTime > this.current) {
  112. if (this.isMarker(candidateTime)) {
  113. nextMarkerTime = candidateTime;
  114. break;
  115. } else if (this.isExitAction(candidateTime)) {
  116. tickPromises.push(this._executeTick(candidateTime));
  117. }
  118. }
  119. }
  120. if (this.isPlaying() && nextMarkerTime !== this.duration) {
  121. this.pause();
  122. tickPromises.push(this._executeTick(nextMarkerTime).then(this.play.bind(this)));
  123. } else {
  124. tickPromises.push(this._executeTick(nextMarkerTime));
  125. }
  126. return Promise.all(tickPromises);
  127. },
  128. jumpToPreviousMarker: function jumpToPreviousMarker() {
  129. var previousMarkerTime = 0;
  130. var eventTimes = Object.keys(this._eventQueue);
  131. for (var i = 1; i <= eventTimes.length; i++) {
  132. var candidateTime = parseInt(eventTimes[eventTimes.length - i], 10);
  133. if (candidateTime < this.current && this.isMarker(candidateTime)) {
  134. previousMarkerTime = candidateTime;
  135. break;
  136. }
  137. }
  138. if (this.isPlaying()) {
  139. this.pause();
  140. }
  141. return this.seek(previousMarkerTime);
  142. },
  143. _playScene: function _playScene() {
  144. var ret = false;
  145. if (this.currentScene) {
  146. if (this.current >= this.duration) {
  147. this.current = 0;
  148. }
  149. this.seek(this.current);
  150. this._setState(this.STATE.PLAYING);
  151. this._startTime = Date.now() - this.current;
  152. this._timer = setInterval(this._executeTick.bind(this), this._tickLength);
  153. ret = true;
  154. }
  155. return ret;
  156. },
  157. _generateEventQueue: function _generateEventQueue() {
  158. this._eventQueue = {};
  159. var nMaxTime = 0;
  160. var widgetIds = this._listWidgetsForCurrentScene();
  161. _.each(widgetIds, function (widgetId) {
  162. var episode = this.boardModel.timeline.episodes.get(widgetId);
  163. if (episode && !episode.acts.isEmpty()) {
  164. nMaxTime = this._addEpisodeToEventQueue(widgetId, episode, nMaxTime);
  165. } else {
  166. // No time events defined. Assume visible always
  167. this._addEventToQueue(widgetId, 0, 'show');
  168. }
  169. }.bind(this));
  170. // Don't generate events for the end-most widgets in the timequeue.
  171. _.each(Object.keys(this._eventQueue), function (key) {
  172. if (key > nMaxTime - this._endBufferTime) {
  173. delete this._eventQueue[key];
  174. }
  175. }.bind(this));
  176. return nMaxTime;
  177. },
  178. _addEpisodeToEventQueue: function _addEpisodeToEventQueue(widgetId, episode, nMaxTime) {
  179. episode.acts.each(function (act) {
  180. if (act.timer || act.timer === 0) {
  181. var time = this._getEventTickTime(act.timer);
  182. this._addEventToQueue(widgetId, time, act.action, act.payload);
  183. nMaxTime = Math.max(nMaxTime, time);
  184. }
  185. }.bind(this));
  186. return nMaxTime;
  187. },
  188. _addEventToQueue: function _addEventToQueue(widgetId, time, action, payload) {
  189. if (!this._eventQueue[time]) {
  190. this._eventQueue[time] = [];
  191. }
  192. this._eventQueue[time].push({
  193. action: action,
  194. payload: payload,
  195. widget: widgetId
  196. });
  197. },
  198. _getEventTickTime: function _getEventTickTime(time) {
  199. return Math.round(time / this._tickLength) * this._tickLength;
  200. },
  201. pause: function pause() {
  202. if (!this._timer) {
  203. return false;
  204. }
  205. clearInterval(this._timer);
  206. this._timer = null;
  207. this._setState(this.STATE.PAUSED);
  208. return true;
  209. },
  210. stop: function stop() {
  211. if (this._timer) {
  212. clearInterval(this._timer);
  213. this._timer = null;
  214. }
  215. this._setState(this.STATE.STOPPED);
  216. return this.seek(0);
  217. },
  218. refresh: function refresh() {
  219. // Clear the duration to force a regeneration of the event queue.
  220. this.duration = 0;
  221. this.seek(this.current);
  222. },
  223. reset: function reset() {
  224. // Clear the duration to force a regeneration of the event queue.
  225. this.duration = 0;
  226. this.current = 0;
  227. return this.stop();
  228. },
  229. seek: function seek(offset) {
  230. var _this = this;
  231. if (!this.currentScene) {
  232. this._triggerUpdateDuration();
  233. this._triggerTickUpdated();
  234. return Promise.resolve(false);
  235. }
  236. if (this.currentState === this.STATE.PLAYING) {
  237. this.pause();
  238. }
  239. if (!this.duration) {
  240. this.duration = this._generateEventQueue();
  241. this._triggerUpdateDuration();
  242. }
  243. var nOffset = Math.max(0, Math.min(this.duration, this._getEventTickTime(offset)));
  244. this.current = nOffset;
  245. return this._setInitialWidgetState().then(function () {
  246. var moreToPlay = false;
  247. if (nOffset < _this.duration) {
  248. moreToPlay = true;
  249. }
  250. _this._triggerTickUpdated();
  251. return moreToPlay;
  252. });
  253. },
  254. getState: function getState() {
  255. var oState = {
  256. currentState: this.currentState,
  257. currentTime: this.current,
  258. duration: this.duration,
  259. sceneId: this.currentScene,
  260. context: this
  261. };
  262. return oState;
  263. },
  264. getDuration: function getDuration() {
  265. return this.duration;
  266. },
  267. getMarkers: function getMarkers() {
  268. var eventTimes = _.map(Object.keys(this._eventQueue), function (timeString) {
  269. return parseInt(timeString, 10);
  270. });
  271. return eventTimes.filter(this.isMarker.bind(this));
  272. },
  273. endScene: function endScene() {
  274. return this.seek(this.duration);
  275. },
  276. setTickLength: function setTickLength(tickLength) {
  277. this.stop();
  278. this._tickLength = tickLength;
  279. },
  280. getTickLength: function getTickLength() {
  281. return this._tickLength;
  282. },
  283. _executeTick: function _executeTick(current) {
  284. var target = Date.now() - this._startTime;
  285. var eventPromises = [];
  286. // If we are late, catch up to where we should be right now.
  287. while (target >= this.current + this._tickLength || _.isNumber(current)) {
  288. if (_.isNumber(current)) {
  289. this.current = current;
  290. } else {
  291. this.current += this._tickLength;
  292. }
  293. //Check if we have events to dispatch
  294. if (this._eventQueue[this.current]) {
  295. _.each(this._eventQueue[this.current], function (event) {
  296. eventPromises.push(this._dispatchEvent(event));
  297. }.bind(this));
  298. }
  299. this._triggerTickUpdated();
  300. if (this.current >= this.duration) {
  301. this.current = this.duration;
  302. var endState = this.currentState;
  303. this._setState(this.STATE.STOPPED);
  304. clearInterval(this._timer);
  305. this._timer = null;
  306. this.eventRouter.trigger('timeline:end', {
  307. endState: endState
  308. });
  309. break;
  310. }
  311. if (_.isNumber(current)) {
  312. break;
  313. }
  314. }
  315. return Promise.all(eventPromises);
  316. },
  317. _triggerTickUpdated: function _triggerTickUpdated() {
  318. this.eventRouter.trigger('timequeue:tick', { currentTime: this.current, sceneId: this.currentScene });
  319. },
  320. _setState: function _setState(newState) {
  321. var sPrevState = this.currentState;
  322. this.currentState = newState;
  323. this.eventRouter.trigger('timequeue:stateChanged', {
  324. currentState: this.currentState,
  325. prevState: sPrevState,
  326. currentTime: this.current,
  327. duration: this.duration,
  328. sceneId: this.currentScene,
  329. context: this
  330. });
  331. },
  332. _dispatchEvent: function _dispatchEvent(event, immediate) {
  333. var nAnimationDuration = immediate ? 0 : this.animationDuration;
  334. var oAnimation = {
  335. duration: nAnimationDuration,
  336. target: event.widget,
  337. payload: event.payload,
  338. animation: this._actionToAnimationMap[event.action].name,
  339. reveal: nAnimationDuration > 0 && this._actionToAnimationMap[event.action].entrance
  340. };
  341. if (!oAnimation.animation) {
  342. //Unknown animation, do nothing.
  343. return Promise.resolve();
  344. }
  345. this._updateEventForStateGroup(this._widgetStateCache, event);
  346. var state = this.widgetHelper.getContentState(oAnimation.target);
  347. if (state) {
  348. return state.whenStatusChanges(state.STATUS.RENDERED).then(function () {
  349. this.eventRouter.trigger('widget:animate', oAnimation);
  350. }.bind(this));
  351. }
  352. return Promise.resolve();
  353. },
  354. _setInitialWidgetState: function _setInitialWidgetState() {
  355. var _this2 = this;
  356. var stateGroups = {};
  357. var widgetPromises = [];
  358. this._hideAllWidgets(stateGroups);
  359. //Apply any events that happen between 0 and the current time
  360. var i = void 0,
  361. j = void 0;
  362. for (i = 0; i <= this.current; i += this._tickLength) {
  363. if (this._eventQueue[i]) {
  364. for (j = 0; j < this._eventQueue[i].length; j++) {
  365. this._updateEventForStateGroup(stateGroups, this._eventQueue[i][j]);
  366. }
  367. }
  368. }
  369. var _isEqual = function _isEqual(a, b) {
  370. var animationA = _this2._actionToAnimationMap[a.action];
  371. var animationB = _this2._actionToAnimationMap[b.action];
  372. return animationA.entrance === animationB.entrance && animationA.stateGroup === 'visibility' && animationB.stateGroup === 'visibility' || _.isEqual(a, b);
  373. };
  374. // at this point stateGroups contains the final state of all the widgets @ this.current time
  375. // if any of those states is different from the previous time we where here, we apply them.
  376. // Doing this prevents sending lots of duplicate events durring seek.
  377. Object.keys(stateGroups).forEach(function (key) {
  378. var group = stateGroups[key];
  379. Object.keys(group).forEach(function (widgetId) {
  380. if (!this._widgetStateCache[key] || !this._widgetStateCache[key][widgetId] || !_isEqual(this._widgetStateCache[key][widgetId], group[widgetId])) {
  381. widgetPromises.push(this._dispatchEvent(group[widgetId], true));
  382. }
  383. }, this);
  384. }, this);
  385. return Promise.all(widgetPromises);
  386. },
  387. _hideAllWidgets: function _hideAllWidgets(stateGroups) {
  388. var widgets = this._listWidgetsForCurrentScene() || [];
  389. widgets.forEach(function (widgetId) {
  390. this._updateEventForStateGroup(stateGroups, {
  391. action: 'hide',
  392. widget: widgetId
  393. });
  394. this._updateEventForStateGroup(stateGroups, {
  395. action: 'clearHighlight',
  396. widget: widgetId
  397. });
  398. }, this);
  399. },
  400. _updateEventForStateGroup: function _updateEventForStateGroup(stateGroups, event) {
  401. var groupName = this._actionToAnimationMap[event.action].stateGroup;
  402. if (!stateGroups[groupName]) {
  403. stateGroups[groupName] = {};
  404. }
  405. stateGroups[groupName][event.widget] = event;
  406. },
  407. _triggerUpdateDuration: function _triggerUpdateDuration() {
  408. this.eventRouter.trigger('timequeue:durationChanged', { duration: this.duration, sceneId: this.currentScene });
  409. },
  410. _listWidgetsForCurrentScene: function _listWidgetsForCurrentScene() {
  411. if (this.currentScene && this.boardModel) {
  412. return this.boardModel.layout.listWidgets([this.currentScene]);
  413. }
  414. return [];
  415. }
  416. });
  417. return TimeQueue;
  418. });
  419. //# sourceMappingURL=TimeQueue.js.map