TimelineController.js 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121
  1. 'use strict';
  2. /*
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: Storytelling
  5. * (C) Copyright IBM Corp. 2014, 2019
  6. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  7. */
  8. define(['baglass/core-client/js/core-client/ui/core/Events', 'dashboard-analytics/apiHelpers/SlotAPIHelper', 'underscore', './AnimationDirector', './TimeQueue', './util/WidgetHelper'], function (Events, SlotAPIHelper, _, AnimationDirector, TimeQueue, WidgetHelper) {
  9. var Controller = Events.extend({
  10. authoring: false,
  11. duration: 10000,
  12. defaultWidgetDuration: 5000,
  13. animationDuration: 500,
  14. cursorTime: 0,
  15. suggestionTimelineCheckingInterval: 100,
  16. suggestionTimeRange: [],
  17. lastEstablishedEventQueue: [],
  18. init: function init(options) {
  19. Controller.inherited('init', this, arguments);
  20. this.canvasController = options.canvasController;
  21. this.dashboardApi = options.dashboardApi;
  22. this.timeline = options.model.timeline;
  23. this.layout = options.model.layout;
  24. this.widgetInstances = options.model.widgetInstances;
  25. this.model = options.model;
  26. this.eventRouter = options.eventRouter;
  27. this.timeQueues = {};
  28. this.selectedWidgetIds = {};
  29. this.authoring = options.authoring;
  30. this.services = options.services;
  31. this.stringResources = this.services.getSvcSync('.StringResources');
  32. this.widgetHelper = new WidgetHelper({ dashboardApi: this.dashboardApi });
  33. if (this.model.pageContext) {
  34. this._animationDirector = new AnimationDirector({
  35. widgetHelper: this.widgetHelper,
  36. pageContextAPI: this.model.pageContext.getAPI()
  37. });
  38. }
  39. this.canvas = this.dashboardApi.getCanvas();
  40. //Listen for events
  41. this.eventRouter.on('widget:maximize', this._onMaximizeWidget, this);
  42. this.eventRouter.on('widget:restore', this._onRestoreWidget, this);
  43. this.eventRouter.on('widget:selected', this.onSelectWidget, this);
  44. this.eventRouter.on('widget:deselected', this.onDeselectWidget, this);
  45. this.eventRouter.on('timequeue:tick', this.onTimeQueueTick, this);
  46. this.eventRouter.on('timequeue:stateChanged', this.onTimeQueueStateChanged, this);
  47. this.eventRouter.on('timequeue:durationChanged', this.onDurationChanged, this);
  48. this.eventRouter.on('navigation:complete', this.onNavigationComplete, this);
  49. this.eventRouter.on('scene:reorder', this.onSceneReorder, this);
  50. this.eventRouter.on('scene:swap', this.onSceneSwap, this);
  51. this.eventRouter.on('mode:change', this.onModeChanged, this);
  52. this.eventRouter.on('rendered', this.onWidgetRendered, this);
  53. this.eventRouter.on('widget:animate', this.onAnimate, this);
  54. this.eventRouter.on('layoutType:changed', this._onLayoutTypeChanged, this);
  55. this.eventRouter.on('timeline:end', this.onTimelinePlaybackEnd, this);
  56. this.timeline.on('change:kioskMode', this.onKioskModeChange, this);
  57. this.timeline.on('change:navigateMarkers', this.onNavigateMarkersChange, this);
  58. this.timeline.on('change:episodes', this.onModelEpisodeChange, this);
  59. this.timeline.on('timeline:episodeFragmentAdded', this.onEpisodeFragmentAdded, this);
  60. this.timeline.episodes.on('add', this.onEpisodeAdded, this);
  61. this.timeline.episodes.on('remove', this.onEpisodeRemoved, this);
  62. this.model.on('widget:change', this.onChangeWidget, this);
  63. },
  64. destroy: function destroy() {
  65. // Please keep the same order as registering events for easier maintenance.
  66. this.eventRouter.off('widget:maximize', this._onMaximizeWidget, this);
  67. this.eventRouter.off('widget:restore', this._onRestoreWidget, this);
  68. this.eventRouter.off('widget:selected', this.onSelectWidget, this);
  69. this.eventRouter.off('widget:deselected', this.onDeselectWidget, this);
  70. this.eventRouter.off('timequeue:tick', this.onTimeQueueTick, this);
  71. this.eventRouter.off('timequeue:stateChanged', this.onTimeQueueStateChanged, this);
  72. this.eventRouter.off('timequeue:durationChanged', this.onDurationChanged, this);
  73. this.eventRouter.off('navigation:complete', this.onNavigationComplete, this);
  74. this.eventRouter.off('scene:reorder', this.onSceneReorder, this);
  75. this.eventRouter.off('scene:swap', this.onSceneSwap, this);
  76. this.eventRouter.off('mode:change', this.onModeChanged, this);
  77. this.eventRouter.off('rendered', this.onWidgetRendered, this);
  78. this.eventRouter.off('widget:animate', this.onAnimate, this);
  79. this.eventRouter.off('layoutType:changed', this._onLayoutTypeChanged, this);
  80. this.eventRouter.off('timeline:end', this.onTimelinePlaybackEnd, this);
  81. this.timeline.off('change:kioskMode', this.onKioskModeChange, this);
  82. this.timeline.off('change:navigateMarkers', this.onNavigateMarkersChange, this);
  83. this.timeline.off('change:episodes', this.onModelEpisodeChange, this);
  84. this.timeline.off('timeline:episodeFragmentAdded', this.onEpisodeFragmentAdded, this);
  85. this.timeline.episodes.off('add', this.onEpisodeAdded, this);
  86. this.timeline.episodes.off('remove', this.onEpisodeRemoved, this);
  87. this.model.off('widget:change', this.onChangeWidget, this);
  88. var that = this;
  89. _.each(this.widgetInstances, function (widgetInstance) {
  90. var widget = that.widgetHelper.getWidget(widgetInstance.getId());
  91. if (widget && widget.getVisApi && widget.getVisApi() && widget.getVisApi().ownerWidget) {
  92. widget.getVisApi().ownerWidget.off('dwChange:visTransaction', that.onWidgetChange, that);
  93. widget.getVisApi().ownerWidget.off('dwChange:visId', that.onWidgetChange, that);
  94. }
  95. });
  96. },
  97. _onLayoutTypeChanged: function _onLayoutTypeChanged() {
  98. this.timeQueues = {};
  99. this.scene = null;
  100. },
  101. onWidgetChange: function onWidgetChange(event) {
  102. // in the case of undo/redo we have already did have an entry on the undo/redo stack so we don't need to do anything
  103. // (also for undo/redo of dwChange:visId the events are in the wrong order so best to just ignore)
  104. if (!(event && event.model && event.model.id && event.sender !== 'UndoRedoController')) {
  105. return;
  106. }
  107. var options = event.options || {};
  108. options.payloadData = event.data;
  109. options.sender = event.sender;
  110. options = this._validateModelOptions(options);
  111. var episode = this.getTimelineEpisodeById(event.model.id);
  112. if (episode) {
  113. //grab the default "empty" and send it off the the model
  114. var blankPayloadExemplar = this._getTimelineHighlightsForWidget(event.model.id);
  115. episode.acts.updateHighlightPayloadColumns(blankPayloadExemplar, options);
  116. }
  117. },
  118. setScene: function setScene(scene) {
  119. if (scene) {
  120. if (this.scene !== scene) {
  121. this.scene = scene;
  122. this.setCursorTime(0);
  123. }
  124. } else {
  125. this.scene = null;
  126. this.trigger('time:update', { currentTime: 0 });
  127. }
  128. },
  129. onWidgetRendered: function onWidgetRendered(api) {
  130. if (api) {
  131. this.trigger('slider:change', {
  132. 'name': 'rendered',
  133. 'modelId': api.getId()
  134. });
  135. }
  136. var widget = this.widgetHelper.getWidget(api.getId());
  137. if (widget && widget.getVisApi && widget.getVisApi() && widget.getVisApi().ownerWidget) {
  138. // deregister first and re-register
  139. widget.getVisApi().ownerWidget.off('dwChange:visTransaction', this.onWidgetChange, this);
  140. widget.getVisApi().ownerWidget.on('dwChange:visTransaction', this.onWidgetChange, this);
  141. widget.getVisApi().ownerWidget.off('dwChange:visId', this.onWidgetChange, this);
  142. widget.getVisApi().ownerWidget.on('dwChange:visId', this.onWidgetChange, this);
  143. }
  144. },
  145. onRemoveLayout: function onRemoveLayout(evt) {
  146. _.each(evt.value.parameter, function (sceneId) {
  147. if (this.timeQueues[sceneId]) {
  148. delete this.timeQueues[sceneId];
  149. }
  150. }.bind(this));
  151. },
  152. _seekStory: function _seekStory(sceneIndex) {
  153. if (this.layout.type === 'slideshow') {
  154. // scene preparation is limited to three scenes in slideshow
  155. var timeQueue = this._currentTimeQueue();
  156. if (timeQueue) {
  157. timeQueue.pause();
  158. }
  159. var sceneCount = this.model.layout.items.length;
  160. this._maxScene(sceneIndex === 0 ? sceneCount - 1 : sceneIndex - 1);
  161. this._zeroScene(sceneIndex);
  162. this._zeroScene(sceneIndex === sceneCount - 1 ? 0 : sceneIndex + 1);
  163. } else {
  164. this.maxPreviousScenes(sceneIndex);
  165. this.zeroFutureScenes(sceneIndex);
  166. }
  167. },
  168. onNavigationComplete: function onNavigationComplete(evt) {
  169. var play;
  170. if (evt) {
  171. play = evt.play;
  172. this._seekStory(evt.index);
  173. }
  174. // In consumption mode we auto play when we switch scenes unless an explicit play instruction is received
  175. if (play !== false) {
  176. if (!this.isAuthoring() && !this.isPlaying() && !this.isNavigateMarkers() || play) {
  177. this.play();
  178. }
  179. }
  180. },
  181. isAuthoring: function isAuthoring() {
  182. return this.authoring;
  183. },
  184. onModeChanged: function onModeChanged(event) {
  185. this.authoring = event.authoring;
  186. },
  187. onSceneReorder: function onSceneReorder(event) {
  188. this.trigger('scene:reorder', event);
  189. },
  190. maxPreviousScenes: function maxPreviousScenes(idx) {
  191. var seekPromises = [];
  192. if (idx === -2) {
  193. // Special case - end of story!
  194. idx = this.model.layout.items.length;
  195. }
  196. for (var i = 0; i < idx; i++) {
  197. seekPromises.push(this._maxScene(i));
  198. }
  199. return Promise.all(seekPromises);
  200. },
  201. zeroFutureScenes: function zeroFutureScenes(idx) {
  202. var seekPromises = [];
  203. if (idx !== -2) {
  204. var sceneCount = this.model.layout.items.length;
  205. var i = idx;
  206. if (i < 0) {
  207. i = 0;
  208. }
  209. for (; i < sceneCount; i++) {
  210. seekPromises.push(this._zeroScene(i));
  211. }
  212. }
  213. return Promise.all(seekPromises);
  214. },
  215. _zeroScene: function _zeroScene(index) {
  216. var scene = this.model.layout.items[index];
  217. var timeQueue = this._getTimeQueue(scene.id);
  218. if (timeQueue.isPlaying()) {
  219. timeQueue.stop();
  220. }
  221. return timeQueue.reset();
  222. },
  223. _maxScene: function _maxScene(index) {
  224. var scene = this.model.layout.items[index];
  225. var timeQueue = this._getTimeQueue(scene.id);
  226. if (timeQueue.isPlaying()) {
  227. timeQueue.stop();
  228. }
  229. return timeQueue.endScene();
  230. },
  231. getCurrentScene: function getCurrentScene() {
  232. return this.scene;
  233. },
  234. getTimelineEpisodeCount: function getTimelineEpisodeCount() {
  235. return this._getWidgetIds().length;
  236. },
  237. getTimelineEpisodeById: function getTimelineEpisodeById(id) {
  238. return this._isWidgetInCurrentScene(id) ? this._getEpisodeModel(id) : undefined;
  239. },
  240. /* this method must return the episodes in the order they are in the collection*/
  241. getTimelineEpisodes: function getTimelineEpisodes() {
  242. var widgets = {};
  243. // create an 'index' of the widget ids for faster lookup
  244. this._getWidgetIds().forEach(function (id) {
  245. widgets[id] = id;
  246. });
  247. return _.filter(this.model.timeline.episodes.models, function (episode) {
  248. return episode.id in widgets;
  249. }.bind(this));
  250. },
  251. getTimelineEpisodeIDs: function getTimelineEpisodeIDs() {
  252. var episodeIDs = [];
  253. var episodeObjs = this.getTimelineEpisodes();
  254. Object.keys(episodeObjs).forEach(function (key) {
  255. return episodeIDs.push(episodeObjs[key].id);
  256. });
  257. return episodeIDs;
  258. },
  259. getMarkers: function getMarkers() {
  260. var timeQueue = this._currentTimeQueue();
  261. if (timeQueue) {
  262. return timeQueue.getMarkers();
  263. }
  264. return [];
  265. },
  266. isHighlightSupported: function isHighlightSupported(widgetId) {
  267. var highlights = this._getTimelineHighlightsForWidget(widgetId);
  268. return Boolean(highlights && highlights.length);
  269. },
  270. updateLastEstablishedEventQueue: function updateLastEstablishedEventQueue() {
  271. //get all the start times in timeQueue._eventQueue
  272. this.lastEstablishedEventQueue = [];
  273. var timeQueue = this._currentTimeQueue();
  274. if (timeQueue) {
  275. this._updateLastEstablishedEventQueue(timeQueue);
  276. }
  277. return this.lastEstablishedEventQueue;
  278. },
  279. _updateLastEstablishedEventQueue: function _updateLastEstablishedEventQueue(timeQueue) {
  280. for (var key in timeQueue._eventQueue) {
  281. if (timeQueue._eventQueue.hasOwnProperty(key)) {
  282. for (var selectedWidgetId in this.selectedWidgetIds) {
  283. if (timeQueue._eventQueue[key][0].item !== selectedWidgetId || timeQueue._eventQueue[key].length > 1) {
  284. this.lastEstablishedEventQueue.push(key);
  285. }
  286. }
  287. }
  288. }
  289. },
  290. moveEpisodeBefore: function moveEpisodeBefore(id, beforeId, options) {
  291. options = this._validateModelOptions(options);
  292. this.timeline.episodes.reorder(id, beforeId, options);
  293. },
  294. updateTimelineDuration: function updateTimelineDuration(id, start, end, options) {
  295. options = this._validateModelOptions(options);
  296. if (this.suggestionTimeRange.length > 0) {
  297. start = this.suggestionTimeRange[0];
  298. end = this.suggestionTimeRange[1];
  299. }
  300. var episode = this.timeline.episodes.get(id);
  301. var previousEpisodeActs = _.map(episode.acts.toArray(), _.clone);
  302. var previousEpisodeStart = episode.getEntranceAct().timer;
  303. var previousEpisodeEnd = episode.getExitAct().timer;
  304. var previousDuration = previousEpisodeEnd - previousEpisodeStart;
  305. var newDuration = end - start;
  306. // +- 2 ms is close... this should handle all rounding cases in a way that is not noticeable visually.
  307. // What is important is that we correctly detect all moves. If we detect a resize of 2 ms as a move that is not an issue.
  308. options.isMove = newDuration <= previousDuration + 2 && previousDuration <= newDuration + 2;
  309. // if we are reordering - let the reorder model change handle undo-redo
  310. var isReorder = options.isMove && Math.abs(previousEpisodeStart - start) <= 5;
  311. var notifyUpdateDurationListeners = function (isUndoRedo) {
  312. this.trigger('modelEpisode:changed', {
  313. id: id,
  314. value: [episode.getEntranceAct().timer, episode.getExitAct().timer]
  315. });
  316. this.model.trigger('change', isUndoRedo || isReorder ? {} : {
  317. value: _.map(episode.acts.toArray(), _.clone),
  318. prevValue: previousEpisodeActs,
  319. sender: options.sender,
  320. data: options.payloadData,
  321. senderContext: {
  322. applyFn: function (value) {
  323. episode.acts.set(value, { 'silent': true });
  324. notifyUpdateDurationListeners(true);
  325. }.bind(this)
  326. }
  327. });
  328. this._refreshTimeQueue();
  329. }.bind(this);
  330. episode.updateDuration(start, end, options);
  331. notifyUpdateDurationListeners();
  332. this.suggestionTimeRange = [];
  333. },
  334. _getTimelineHighlightsForWidget: function _getTimelineHighlightsForWidget(contentId) {
  335. var content = this.widgetHelper.getContent(contentId);
  336. var visualization = content.getFeature('Visualization');
  337. // this returns only the mapped data slots
  338. var slots = visualization && visualization.getSlots().getMappedSlotList();
  339. var slotEligible = function slotEligible(slot, dataItem, index) {
  340. // True if type is attribute and slot is not a multi measure series
  341. return !SlotAPIHelper.isMultiMeasuresSeriesOrValue(slot, index) && dataItem.getType() === 'attribute';
  342. };
  343. var highlightPayloads = [];
  344. _.each(slots, function (slot) {
  345. _.each(slot.getDataItemList(), function (dataItem, index) {
  346. var columnId = dataItem.getColumnId();
  347. var label = dataItem.getLabel();
  348. if (slotEligible(slot, dataItem, index) && !_.findWhere(highlightPayloads, {
  349. columnId: columnId
  350. })) {
  351. // using dataItem.getUniqueId() for the ID is problematic since the dataitem
  352. // that gets mapped to each payload entry can change when columns are dragged around, added, or deleted.
  353. highlightPayloads.push({
  354. columnId: columnId,
  355. columnLabel: label,
  356. id: columnId,
  357. values: []
  358. });
  359. }
  360. });
  361. });
  362. return highlightPayloads;
  363. },
  364. addTimelineHighlight: function addTimelineHighlight(widgetId, timer) {
  365. var episodeModel = this._getEpisodeModel(widgetId);
  366. var highlights = this._getTimelineHighlightsForWidget(widgetId);
  367. // default to current time if no timer is passed in
  368. timer = timer || timer === 0 ? timer : this.getCurrentTime();
  369. // we might want that in the timeline models... we need to make sure that the current time is inside the widget time.
  370. // we make ensure that it's at a least 1 ms inside the entrance and exit time.
  371. timer = Math.max(timer, episodeModel.getEntranceAct().timer + 1);
  372. timer = Math.min(timer, episodeModel.getExitAct().timer - 1);
  373. var includesAllAct = {
  374. timer: timer,
  375. action: 'highlight',
  376. payload: highlights
  377. };
  378. var options = {
  379. payloadData: {
  380. undoRedoTransactionId: _.uniqueId('highlight')
  381. }
  382. };
  383. var newAct = episodeModel.acts.add(includesAllAct, options);
  384. this.trigger('slider:showHighlightSummary', {
  385. episodeModel: episodeModel,
  386. actModel: newAct
  387. });
  388. return newAct;
  389. },
  390. updateTimelineHighlight: function updateTimelineHighlight(widgetId, actId, attributes) {
  391. var episodeModel = this._getEpisodeModel(widgetId);
  392. var act = episodeModel.acts.get(actId);
  393. if (act) {
  394. act.set(attributes);
  395. }
  396. return act;
  397. },
  398. deleteTimelineHighlight: function deleteTimelineHighlight(widgetId, actId) {
  399. var episodeModel = this._getEpisodeModel(widgetId);
  400. if (episodeModel && actId) {
  401. var actModel = episodeModel.acts.get(actId);
  402. if (actModel && actModel.action === 'highlight') {
  403. episodeModel.acts.remove(actModel);
  404. }
  405. }
  406. },
  407. getSnapIndicatorTime: function getSnapIndicatorTime(timeRange, draggingElement) {
  408. var timeLength = timeRange[1] - timeRange[0],
  409. newTimeRange = [],
  410. changed = null,
  411. i;
  412. for (i = timeRange.length - 1; i >= 0; i--) {
  413. newTimeRange.unshift(timeRange[i]);
  414. }
  415. var checkTimeQueueResult = this._checkTimeQueueForSnapIndicator(timeRange, draggingElement, newTimeRange, timeLength);
  416. if (checkTimeQueueResult.changed) {
  417. newTimeRange = checkTimeQueueResult.timeRange;
  418. changed = checkTimeQueueResult.changed || changed;
  419. }
  420. var checkStartAndEndResult = this._checkStartAndEndForSnapIndicator(timeRange, draggingElement, newTimeRange);
  421. if (checkStartAndEndResult.changed) {
  422. newTimeRange = checkStartAndEndResult.timeRange;
  423. changed = checkStartAndEndResult.changed || changed;
  424. }
  425. if (draggingElement === 'content') {
  426. if (changed === 'start') {
  427. newTimeRange[1] = newTimeRange[0] + timeLength;
  428. } else if (changed === 'end') {
  429. newTimeRange[0] = newTimeRange[1] - timeLength;
  430. }
  431. }
  432. this.suggestionTimeRange = newTimeRange;
  433. return {
  434. changed: changed,
  435. timeRange: newTimeRange
  436. };
  437. },
  438. selectWidgetAndSlider: function selectWidgetAndSlider(widgetId) {
  439. this.deselectAllWidgetsAndSliders();
  440. this.canvas.selectContent([widgetId], { hideContextBar: true });
  441. return Promise.resolve();
  442. },
  443. deselectWidgetAndSlider: function deselectWidgetAndSlider(widgetId) {
  444. this.deselectAllWidgetsAndSliders();
  445. this.canvas.deselectContent([widgetId]);
  446. return Promise.resolve();
  447. },
  448. deselectAllWidgetsAndSliders: function deselectAllWidgetsAndSliders() {
  449. var selectedContentList = this.canvas.getSelectedContentList();
  450. var selectedContentIdList = selectedContentList.map(function (content) {
  451. return content.getId();
  452. });
  453. this.canvas.deselectContent(selectedContentIdList);
  454. },
  455. getDefaultWidgetDuration: function getDefaultWidgetDuration() {
  456. return this.defaultWidgetDuration;
  457. },
  458. getDuration: function getDuration() {
  459. var timeQueue = this._currentTimeQueue();
  460. return timeQueue ? timeQueue.getDuration() : 0;
  461. },
  462. isAtEndOfScene: function isAtEndOfScene() {
  463. return this.getCurrentTime() === this.getDuration();
  464. },
  465. isAtStartOfScene: function isAtStartOfScene() {
  466. return this.getCurrentTime() === 0;
  467. },
  468. togglePlayThrough: function togglePlayThrough() {
  469. this.timeline.set({
  470. playThrough: !this.isPlayThrough()
  471. }, {
  472. payloadData: {
  473. undoRedoTransactionId: _.uniqueId('togglePlayThrough')
  474. }
  475. });
  476. },
  477. toggleKioskMode: function toggleKioskMode() {
  478. this.timeline.set({
  479. kioskMode: !this.isKioskMode()
  480. }, {
  481. payloadData: {
  482. undoRedoTransactionId: _.uniqueId('toggleKioskMode')
  483. }
  484. });
  485. },
  486. toggleNavigateMarkers: function toggleNavigateMarkers() {
  487. this.timeline.set({
  488. navigateMarkers: !this.isNavigateMarkers()
  489. }, {
  490. payloadData: {
  491. undoRedoTransactionId: _.uniqueId('toggleNavigateMarkers')
  492. }
  493. });
  494. },
  495. toggleRefreshData: function toggleRefreshData() {
  496. this.timeline.set({
  497. refreshData: !this.isRefreshData()
  498. }, {
  499. payloadData: {
  500. undoRedoTransactionId: _.uniqueId('toggleRefreshData')
  501. }
  502. });
  503. },
  504. isPlayThrough: function isPlayThrough() {
  505. return this.timeline.get('playThrough');
  506. },
  507. isKioskMode: function isKioskMode() {
  508. return this.timeline.get('kioskMode');
  509. },
  510. isNavigateMarkers: function isNavigateMarkers() {
  511. return this.timeline.get('navigateMarkers');
  512. },
  513. isRefreshData: function isRefreshData() {
  514. return this.timeline.get('refreshData');
  515. },
  516. jumpToNextMarker: function jumpToNextMarker() {
  517. var timeQueue = this._currentTimeQueue();
  518. return timeQueue.jumpToNextMarker();
  519. },
  520. jumpToPreviousMarker: function jumpToPreviousMarker() {
  521. var timeQueue = this._currentTimeQueue();
  522. return timeQueue.jumpToPreviousMarker();
  523. },
  524. setCurrentTime: function setCurrentTime(time) {
  525. var _this = this;
  526. var ret = false;
  527. var timeQueue = this._currentTimeQueue();
  528. if (timeQueue) {
  529. return timeQueue.seek(time).then(function (result) {
  530. _this._animationDirector.finishWidgetsAnimation();
  531. return result;
  532. });
  533. }
  534. // TO DO: Need to look into to see finishWidgetsAnimation() needs promise chaining.
  535. this._animationDirector.finishWidgetsAnimation();
  536. return Promise.resolve(ret);
  537. },
  538. getCurrentTime: function getCurrentTime() {
  539. var timeQueue = this._currentTimeQueue();
  540. return timeQueue ? timeQueue.getState().currentTime : 0;
  541. },
  542. setCursorTime: function setCursorTime(time) {
  543. this.cursorTime = time;
  544. },
  545. getCursorTime: function getCursorTime() {
  546. return this.cursorTime;
  547. },
  548. getTickDuration: function getTickDuration() {
  549. var timeQueue = this._currentTimeQueue();
  550. return timeQueue ? timeQueue.getTickLength() : 0;
  551. },
  552. isPlaying: function isPlaying() {
  553. var timeQueue = this._currentTimeQueue();
  554. return timeQueue ? timeQueue.isPlaying() : false;
  555. },
  556. isStopped: function isStopped() {
  557. var timeQueue = this._currentTimeQueue();
  558. return timeQueue ? timeQueue.isStopped() : true;
  559. },
  560. pause: function pause() {
  561. var ret = false;
  562. var timeQueue = this._currentTimeQueue();
  563. if (timeQueue) {
  564. ret = timeQueue.pause();
  565. }
  566. this._animationDirector.pauseWidgetsAnimation();
  567. return ret;
  568. },
  569. play: function play() {
  570. var ret = false;
  571. var timeQueue = this._currentTimeQueue();
  572. if (timeQueue) {
  573. this.deselectAllWidgetsAndSliders();
  574. ret = timeQueue.play();
  575. }
  576. this._animationDirector.resumeWidgetsAnimation();
  577. return ret;
  578. },
  579. stop: function stop() {
  580. var ret = false;
  581. var timeQueue = this._currentTimeQueue();
  582. if (timeQueue) {
  583. ret = timeQueue.stop();
  584. }
  585. this._animationDirector.finishWidgetsAnimation();
  586. return ret;
  587. },
  588. isWidgetSelected: function isWidgetSelected(id) {
  589. return this.selectedWidgetIds[id] ? true : false;
  590. },
  591. getSelectedWidgetMap: function getSelectedWidgetMap() {
  592. return this.selectedWidgetIds;
  593. },
  594. getTimeComponents: function getTimeComponents(milliseconds) {
  595. var remainingTime = milliseconds || 0;
  596. var hours = Math.floor(remainingTime / (60 * 60 * 1000));
  597. remainingTime -= hours * 60 * 60 * 1000;
  598. var minutes = Math.floor(remainingTime / (60 * 1000));
  599. remainingTime -= minutes * 60 * 1000;
  600. var seconds = Math.floor(remainingTime / 1000);
  601. remainingTime -= seconds * 1000;
  602. return {
  603. hours: hours,
  604. minutes: minutes,
  605. seconds: seconds,
  606. milliseconds: remainingTime
  607. };
  608. },
  609. getTimeLabel: function getTimeLabel(milliseconds, precision) {
  610. var components = this.getTimeComponents(milliseconds);
  611. // Truncate to 1 decimal place.
  612. var tenthSeconds = Math.round(components.milliseconds / 100);
  613. if (tenthSeconds >= 10) {
  614. // There was rounding to 10; we only want 1 decimal place, so round up to a second.
  615. tenthSeconds = 0;
  616. components.seconds++;
  617. }
  618. var label = '';
  619. var hoursLabel = components.hours > 0 ? components.hours + ':' : '';
  620. var minutesLabel = components.minutes + ':';
  621. var secondsLabel = components.seconds;
  622. if (components.seconds < 10) {
  623. secondsLabel = '0' + secondsLabel;
  624. }
  625. label = label.concat(hoursLabel);
  626. label = label.concat(minutesLabel);
  627. label = label.concat(secondsLabel);
  628. if (precision) {
  629. label = label.concat('.' + tenthSeconds);
  630. }
  631. return label;
  632. },
  633. /*
  634. * Event handlers.
  635. */
  636. onSelectWidget: function onSelectWidget(event) {
  637. this.selectedWidgetIds[event.sender] = true;
  638. this._selectTimelineSlider(event.sender);
  639. },
  640. onDeselectWidget: function onDeselectWidget(event) {
  641. delete this.selectedWidgetIds[event.sender];
  642. this._deselectTimelineSlider(event.sender);
  643. },
  644. onChangeWidget: function onChangeWidget(event) {
  645. this.trigger('slider:change', event);
  646. },
  647. onDurationChanged: function onDurationChanged(event) {
  648. if (!this.scene || event.sceneId === this.scene.id) {
  649. this.trigger('duration:changed', event);
  650. }
  651. },
  652. onTimeQueueTick: function onTimeQueueTick(event) {
  653. if (!this.scene || event.sceneId === this.scene.id) {
  654. this._triggerUpdateTime();
  655. }
  656. },
  657. onTimeQueueStateChanged: function onTimeQueueStateChanged(event) {
  658. if (!this.scene || event.sceneId === this.scene.id) {
  659. this._triggerUpdateTime();
  660. this.trigger('playState:change', event);
  661. }
  662. },
  663. onEpisodeAdded: function onEpisodeAdded(event) {
  664. // TODO: the views should probably be listening to the collection directly.
  665. this.trigger('modelEpisode:added', event.model.id);
  666. },
  667. onEpisodeRemoved: function onEpisodeRemoved(event) {
  668. var widget = this.widgetHelper.getWidget(event.model.id);
  669. if (widget && widget.getVisApi && widget.getVisApi() && widget.getVisApi().ownerWidget) {
  670. widget.getVisApi().ownerWidget.off('dwChange:visTransaction', this.onWidgetChange, this);
  671. widget.getVisApi().ownerWidget.off('dwChange:visId', this.onWidgetChange, this);
  672. }
  673. //TODO: the views should probably listen on the collection directly.
  674. this.trigger('modelEpisode:removed', event.model.id);
  675. this._refreshTimeQueue();
  676. },
  677. onSceneSwap: function onSceneSwap(event) {
  678. var _this2 = this;
  679. _.each(event.scenes, function (scene) {
  680. _this2._getTimeQueue(scene.id).refresh();
  681. });
  682. },
  683. onKioskModeChange: function onKioskModeChange(event) {
  684. this.trigger('kioskMode:change', event);
  685. },
  686. onNavigateMarkersChange: function onNavigateMarkersChange(event) {
  687. this.trigger('navigateMarkers:change', event);
  688. },
  689. onModelEpisodeChange: function onModelEpisodeChange(event) {
  690. // we need to rethink this method... all the cases should be different events really. This might mean we need to move some logic to the model.
  691. var timelineEvent = event;
  692. var episodeEvent = event.origCollectionEvent;
  693. var actEvent = event.origCollectionEvent ? event.origCollectionEvent.origCollectionEvent : null;
  694. if (actEvent && actEvent.name === 'add') {
  695. if (actEvent.model && actEvent.model.action === 'highlight') {
  696. this.trigger('slider:addHighlight', {
  697. actModel: actEvent.model,
  698. episodeModel: episodeEvent.model
  699. });
  700. }
  701. } else if (actEvent && actEvent.name === 'remove') {
  702. if (actEvent.model && actEvent.model.action === 'highlight') {
  703. this.trigger('slider:removeHighlight', {
  704. actModel: actEvent.model,
  705. episodeModel: episodeEvent.model
  706. });
  707. }
  708. } else if (episodeEvent && episodeEvent.model) {
  709. var episode = this._getEpisodeModel(episodeEvent.model.id);
  710. if (episode) {
  711. var timeRange = [episode.getEntranceAct().timer, episode.getExitAct().timer];
  712. this.trigger('modelEpisode:changed', {
  713. id: event.origCollectionEvent.model.id,
  714. value: timeRange
  715. });
  716. }
  717. if (actEvent && event.name === 'action') {
  718. this._previewAnimation(event.value, episodeEvent.model, actEvent.model);
  719. }
  720. } else if (timelineEvent.name === 'reorder') {
  721. // order changed, tell everyone about that
  722. this.trigger('modelEpisodes:reorder');
  723. }
  724. this._refreshTimeQueue();
  725. },
  726. onTimelinePlaybackEnd: function onTimelinePlaybackEnd() {
  727. this._animationDirector.finishWidgetsAnimation();
  728. },
  729. _previewAnimation: function _previewAnimation(animation, episode, act) {
  730. var _this3 = this;
  731. var currentTime = this.getCurrentTime();
  732. var entranceTime = episode.getEntranceAct().timer;
  733. var exitTime = episode.getExitAct().timer;
  734. var isVisible = currentTime >= entranceTime && currentTime < exitTime;
  735. var isEntrance = act.timer === entranceTime;
  736. if (isVisible) {
  737. if (isEntrance) {
  738. this._animationDirector.animate({
  739. target: episode.id,
  740. animation: 'hide',
  741. duration: 0
  742. }).then(function () {
  743. _this3._animationDirector.animate({
  744. target: episode.id,
  745. animation: animation,
  746. duration: _this3.animationDuration
  747. });
  748. });
  749. } else {
  750. this._animationDirector.animate({
  751. target: episode.id,
  752. animation: animation,
  753. duration: this.animationDuration
  754. }).then(function () {
  755. _this3._animationDirector.animate({
  756. target: episode.id,
  757. animation: 'show',
  758. duration: 0
  759. });
  760. });
  761. }
  762. } else {
  763. if (isEntrance) {
  764. this._animationDirector.animate({
  765. target: episode.id,
  766. animation: animation,
  767. duration: this.animationDuration
  768. }).then(function () {
  769. _this3._animationDirector.animate({
  770. target: episode.id,
  771. animation: 'hide',
  772. duration: _this3.animationDuration / 2
  773. });
  774. });
  775. } else {
  776. this._animationDirector.animate({
  777. target: episode.id,
  778. animation: 'show',
  779. duration: this.animationDuration / 2
  780. }).then(function () {
  781. _this3._animationDirector.animate({
  782. target: episode.id,
  783. animation: animation,
  784. duration: _this3.animationDuration
  785. });
  786. });
  787. }
  788. }
  789. },
  790. /*
  791. * Helpers.
  792. */
  793. _validateModelOptions: function _validateModelOptions(options) {
  794. options = options || {};
  795. options.sender = options.sender || this;
  796. options.payloadData = options.payloadData || {};
  797. options.payloadData.undoRedoTransactionId = options.payloadData.undoRedoTransactionId || _.uniqueId('TimelineController');
  798. return options;
  799. },
  800. _selectTimelineSlider: function _selectTimelineSlider(widgetId) {
  801. this.trigger('slider:select', {
  802. widgetId: widgetId
  803. });
  804. },
  805. _deselectTimelineSlider: function _deselectTimelineSlider(widgetId) {
  806. this.trigger('slider:deselect', {
  807. widgetId: widgetId
  808. });
  809. },
  810. _refreshTimeQueue: function _refreshTimeQueue() {
  811. var timeQueue = this._currentTimeQueue();
  812. if (timeQueue) {
  813. timeQueue.refresh();
  814. }
  815. },
  816. _triggerUpdateTime: function _triggerUpdateTime() {
  817. var timeQueue = this._currentTimeQueue();
  818. if (timeQueue) {
  819. this.trigger('time:update', timeQueue.getState());
  820. }
  821. },
  822. _getWidgetIds: function _getWidgetIds() {
  823. if (this.scene) {
  824. return this.model.layout.listWidgets([this.scene.id]);
  825. }
  826. return [];
  827. },
  828. _isWidgetInCurrentScene: function _isWidgetInCurrentScene(id) {
  829. return this._getWidgetIds().indexOf(id) !== -1;
  830. },
  831. _getEpisodeModel: function _getEpisodeModel(id) {
  832. return this.model.timeline.episodes.get(id);
  833. },
  834. _getNewWidgetEpisodeInfo: function _getNewWidgetEpisodeInfo() {
  835. var currentTime = this.getCurrentTime();
  836. var duration = this.getDuration();
  837. var widgetDuration = duration - currentTime;
  838. // if we are at the end we use the full duration
  839. if (widgetDuration === 0) {
  840. widgetDuration = this.defaultWidgetDuration;
  841. }
  842. return {
  843. start: currentTime,
  844. end: currentTime + widgetDuration
  845. };
  846. },
  847. /*
  848. * called when an episode fragment is added,
  849. * It's the controller's job to adjust the timers to match the current 'scrubber' position.
  850. */
  851. onEpisodeFragmentAdded: function onEpisodeFragmentAdded(event) {
  852. var options = this._validateModelOptions(event.options || {});
  853. var startingDuration = this.getDuration();
  854. // update the episode with the new duration.
  855. // this is needed since the episode is added in the board model with potentially the default values.
  856. // once we get here we know how to patch those default to take into account the 'scrubber' position.
  857. var info = this._getNewWidgetEpisodeInfo();
  858. this.updateTimelineDuration(event.id, info.start, info.end, options);
  859. var currentDuration = this.getDuration();
  860. // currently the duration is only extended when a widget is added at the end.
  861. // so we use it as a flag
  862. if (currentDuration > startingDuration) {
  863. this.timeline.stretchEndingEpisodes({
  864. currentEndTime: startingDuration,
  865. newEndTime: currentDuration,
  866. subset: this._getWidgetIds()
  867. }, this, options.payloadData);
  868. }
  869. },
  870. _checkTimeQueueForSnapIndicator: function _checkTimeQueueForSnapIndicator(timeRange, draggingElement, newTimeRange) {
  871. var eventQueueLength = this.lastEstablishedEventQueue.length;
  872. if (eventQueueLength === 0) {
  873. return {};
  874. }
  875. var checkingInterval = this.suggestionTimelineCheckingInterval,
  876. changed = null,
  877. isContent,
  878. i;
  879. var checkSnapRange = function checkSnapRange(index, value) {
  880. var timeDifference = Math.abs(timeRange[index] - value),
  881. ret = timeDifference < checkingInterval;
  882. if (ret) {
  883. // convert to string
  884. newTimeRange[index] = +value;
  885. }
  886. return ret;
  887. };
  888. //check the closest time in the event queue
  889. for (i = 0; i < eventQueueLength && !changed; i++) {
  890. isContent = draggingElement === 'content';
  891. if ((isContent || draggingElement === 'leftHandle') && checkSnapRange(0, this.lastEstablishedEventQueue[i])) {
  892. changed = 'start';
  893. }
  894. if ((isContent || draggingElement === 'rightHandle') && checkSnapRange(1, this.lastEstablishedEventQueue[i])) {
  895. changed = 'end';
  896. }
  897. }
  898. return {
  899. timeRange: newTimeRange,
  900. changed: changed
  901. };
  902. },
  903. _checkStartAndEndForSnapIndicator: function _checkStartAndEndForSnapIndicator(timeRange, draggingElement, newTimeRange) {
  904. var changed = null,
  905. start = timeRange[0],
  906. end = timeRange[1],
  907. duration = this.getDuration(),
  908. isContent = draggingElement === 'content';
  909. if ((isContent || draggingElement === 'leftHandle') && start > 0 && start < this.suggestionTimelineCheckingInterval) {
  910. newTimeRange[0] = 0;
  911. changed = 'start';
  912. }
  913. if ((isContent || draggingElement === 'rightHandle') && end < duration && end > duration - this.suggestionTimelineCheckingInterval) {
  914. newTimeRange[1] = duration;
  915. changed = 'end';
  916. }
  917. return {
  918. timeRange: newTimeRange,
  919. changed: changed
  920. };
  921. },
  922. _getTimeQueue: function _getTimeQueue(id) {
  923. var timeQueue = this.timeQueues[id];
  924. if (!timeQueue) {
  925. timeQueue = new TimeQueue({
  926. sceneId: id,
  927. boardModel: this.model,
  928. eventRouter: this.eventRouter,
  929. widgetHelper: this.widgetHelper
  930. });
  931. this.timeQueues[id] = timeQueue;
  932. timeQueue.reset();
  933. }
  934. return timeQueue;
  935. },
  936. _currentTimeQueue: function _currentTimeQueue() {
  937. var ret = null;
  938. if (this.scene && this.scene.id) {
  939. ret = this._getTimeQueue(this.scene.id);
  940. }
  941. return ret;
  942. },
  943. _onMaximizeWidget: function _onMaximizeWidget() {
  944. if (this.isPlaying()) {
  945. this.pause();
  946. this.pausedForMaximize = true;
  947. } else {
  948. this.pausedForMaximize = false;
  949. }
  950. },
  951. _onRestoreWidget: function _onRestoreWidget() {
  952. if (this.pausedForMaximize) {
  953. this.play();
  954. }
  955. },
  956. onAnimate: function onAnimate(event) {
  957. if (this._animationDirector) {
  958. this._animationDirector.animate(event);
  959. }
  960. }
  961. });
  962. return Controller;
  963. });
  964. //# sourceMappingURL=TimelineController.js.map