angular.module("gameViewer", ['mixpanelAPI', 'gameHeaderWithPeriodControl', 'videoPlayer', 'eventTypeFilterGroup']) .directive('gameViewer', function() { return { restrict: 'E', replace: true, scope: { slId: '=', game: '=', playerSelections: '=', teamSelections:'=', includeFailed: '=', teamPerEventId: '=', eventsSelection: '=', canViewClips: '=', permissions: "=", selectedPeriod: '=', selectedPeriodSet: '=', manpowerSituations: '=', selectPeriod: '&', selectManpowerSituation: '&', togglePerTeamEvents:'&', toggleIncludeFailed: '&', toggleAllPlayers: '&', togglePlayerSelection: '&', isSportlogiq: '&', isSportlogiqEventor: '&', getGameTeamsEventData: '&' }, templateUrl: 'views/gameViewer.html', controller: function ($rootScope, $scope, $timeout, $filter, mixpanelAPI, playlistCreator, $http, $q, errorStatusCheck) { $scope.$watch('game', function(newVal, oldVal) { if (newVal) { $scope.viewContext = { view: $scope.slId, homeTeam: $scope.game.home.name, awayTeam: $scope.game.visitors.name, videos: $scope.game.videos } } }); $scope.playlistCreator = playlistCreator; $scope.blurMe = function ($event) { $event.target.blur(); } $scope.getFilteredEventData = function() { // Since this function runs when the template is loaded // the check below avoids a pointless error from being thrown // when the app is loaded if (!$scope.game) return; var filteredData = $filter('playerEventFilter')($scope.game.home.gameEvents, $scope.playerSelections['home']) .concat($filter('playerEventFilter')($scope.game.visitors.gameEvents, $scope.playerSelections['visitors'])); filteredData = $filter('successfulEventFilter')(filteredData, $scope.includeFailed); filteredData = $filter('periodEventFilter')(filteredData, $scope.selectedPeriodSet); filteredData = $filter('manpowerSituationFilter')(filteredData, $scope.manpowerSituations); filteredData = $filter('eventTypeEventFilter')(filteredData, $scope.eventsSelection.set); return filteredData; } $scope.getPlayerFromRoster = function(playerId, teamId) { var returnPlayer = null; var roster = $scope.game.home.gameRoster; if (teamId != $scope.game.home.id) { roster = $scope.game.visitors.gameRoster; } for (var i=0;i { }; $scope.playbackEvent = function (event) { }; $scope.playbackPlayListInternal = function () { // TODO: Convert filterNames & precidates to an object (use key as filter name and value as predicates/data to filter) $scope.playListPlaybackContext.list = $scope.getFilteredEventData(); mixpanelAPI.send('playbackEventList', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId, "numberOfEvents": $scope.playListPlaybackContext.list.length, "homePlayersSelection": $filter('formatPlayerSelection')($scope.playerSelections['home']), "awayPlayersSelection": $filter('formatPlayerSelection')($scope.playerSelections['visitors']), "successfulEventFilter": $scope.includeFailed, "periodEventFilter": $scope.selectedPeriodSet, "manpowerSituationFilter": $scope.manpowerSituations, "eventTypeEventFilter": $scope.eventsSelection.set }, $scope.game )); if ($scope.playListPlaybackContext.list.length > 0) { $scope.playListPlaybackContext.list.sort(function(a, b) { return a.gameTime - b.gameTime; }); if ($scope.playListPlaybackContext.isVideoPlayerActive == false) { $scope.playListPlaybackContext.isVideoPlayerActive = true; $rootScope.$broadcast("eventSelection::Reset"); } for (var i=0; i < $scope.playListPlaybackContext.list.length; i++) { $scope.populateEventPlayerInfo($scope.playListPlaybackContext.list[i]); } $scope.playListPlaybackContext.playbackHandlers.playbackEventList($scope.playListPlaybackContext.list); } } $scope.restartPlayList = function () { $scope.playListPlaybackContext.playbackHandlers.deactivateVideo(); $scope.playbackPlayListInternal(); mixpanelAPI.send('restartPlayList', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId }, $scope.game )); } $scope.togglePerTeamEventsInternal=function(teamId,game){ $scope.togglePerTeamEvents({ teamPerEventId : teamId, game: game }); } $scope.playbackPeriodInternal = function (selectedPeriod) { vidparams = $scope.game.periods[selectedPeriod-1].video; $scope.periodViewContext.playbackHandlers.setVideo({ url: { folder: 'https://' + vidparams['rootURL'] + '/', name: vidparams['fileName'] }, frameRate: vidparams['frameRate'] }, 0); $scope.periodViewContext.isVideoPlayerActive = true; $scope.periodViewContext.playbackHandlers.play(); // Find first face-off in game events for the selected period for (var i=0; i<$scope.game.gameEvents.length; i++) { if ($scope.game.gameEvents[i]['name'] == 'faceoff' && $scope.game.gameEvents[i]['period'] == $scope.selectedPeriod) { var newTarget = $scope.game.gameEvents[i]; break; } } // If exists, set current target as new face-off, set new Frame if (typeof newTarget !== 'undefined') { $scope.periodViewContext.currentTargetFaceOff = newTarget; $scope.periodViewContext.playbackHandlers.seekToFrame(newTarget.frame, function () { // On seek complete, if target is the same, reset target if ($scope.periodViewContext.currentTargetFaceOff.frame == newTarget.frame) { $scope.periodViewContext.currentTargetFaceOff = null; } }); } mixpanelAPI.send('playbackPeriod', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId, "period": selectedPeriod, "speed": $scope.periodViewContext.speed }, $scope.game )); } $scope.togglePlayPauseInternal = function (context) { if (context.isPaused) { context.playbackHandlers.play(); } else { context.playbackHandlers.pause(); } mixpanelAPI.send('togglePlayPause', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId }, $scope.game )); } $scope.onEventPlaybackStart = function (id, event) { mixpanelAPI.send('playbackStart', mixpanelAPI.decorateDetailsWithPlayerInfo( mixpanelAPI.decorateDetailsWithGameInfo( mixpanelAPI.decorateDetailsWithTeamInfo ({ "view": $scope.slId, "event": event.id, "eventName": event.name, "shorthand": event.shorthand, "period": event.period, "toFrame": event.frame }, event.playerInfo.team), $scope.game), event.playerInfo )); } $scope.onTogglePlayPause = function () { mixpanelAPI.send('togglePlayPause', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId }, $scope.game )); } $scope.onSkipFwd = function () { mixpanelAPI.send('skipFwd', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId }, $scope.game )); } $scope.onSkipBack = function () { mixpanelAPI.send('skipBack', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId }, $scope.game )); } $scope.setIsSeeking = function (isSeekingFlag) { $scope.periodViewContext.isClipSeeking = isSeekingFlag; $scope.playerShiftPlaybackContext.isClipSeeking = isSeekingFlag; if ($scope.canViewClips === true) { $scope.playerShiftPlaybackContext.showVideoPlaceholder = isSeekingFlag; $scope.periodViewContext.showVideoPlaceholder = isSeekingFlag; } }; $scope.nextFaceOff = function () { // if current target is not set, get current frame as current target if ($scope.periodViewContext.currentTargetFaceOff == null) { var currentTargetFrame = $scope.periodViewContext.playbackHandlers.getCurrentFrame(); } else { var currentTargetFrame = $scope.periodViewContext.currentTargetFaceOff.frame; } // Find next face-off in game events from current target for (var i=0; i<$scope.game.gameEvents.length; i++) { if ($scope.game.gameEvents[i]['name'] == 'faceoff' && $scope.game.gameEvents[i]['period'] == $scope.selectedPeriod && $scope.game.gameEvents[i]['frame'] > currentTargetFrame) { var newTarget = $scope.game.gameEvents[i]; break; } } // If exists, set current target as new face-off, set new Frame if (typeof newTarget !== 'undefined') { $scope.periodViewContext.currentTargetFaceOff = newTarget; $scope.periodViewContext.playbackHandlers.seekToFrame(newTarget.frame, function () { // On seek complete, if target is the same, reset target if ($scope.periodViewContext.currentTargetFaceOff.frame == newTarget.frame) { $scope.periodViewContext.currentTargetFaceOff = null; } }); } mixpanelAPI.send('nextFaceOff', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId }, $scope.game )); } $scope.previousFaceOff = function () { // if current target is not set, get current frame as current target if ($scope.periodViewContext.currentTargetFaceOff == null) { var currentTargetFrame = $scope.periodViewContext.playbackHandlers.getCurrentFrame(); } else { var currentTargetFrame = $scope.periodViewContext.currentTargetFaceOff.frame; } // Find previous face-off in game events from current target for (var i=0; i<$scope.game.gameEvents.length; i++) { if ($scope.game.gameEvents[i]['name'] == 'faceoff' && $scope.game.gameEvents[i]['period'] == $scope.selectedPeriod && $scope.game.gameEvents[i]['frame'] < currentTargetFrame) { var newTarget = $scope.game.gameEvents[i]; } if ($scope.game.gameEvents[i]['period'] == $scope.selectedPeriod && $scope.game.gameEvents[i]['frame'] > currentTargetFrame) { break; } } if (typeof newTarget !== 'undefined') { $scope.periodViewContext.currentTargetFaceOff = newTarget; $scope.periodViewContext.playbackHandlers.seekToFrame(newTarget.frame, function () {}); // Give the user some time to skip to some other previous face-off $timeout(function() { if ($scope.periodViewContext.currentTargetFaceOff.frame == newTarget.frame) { $scope.periodViewContext.currentTargetFaceOff = null; } }, 1250); } mixpanelAPI.send('previousFaceOff', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId }, $scope.game )); } $scope.onSetSpeed = function (id, newSpeed) { $scope.setPlaybackSpeed(newSpeed); } $scope.setPlaybackSpeed = function (newSpeed) { $scope.periodViewContext.speed = newSpeed; $scope.periodViewContext.playbackHandlers.setSpeed(newSpeed); $scope.playListPlaybackContext.playbackHandlers.setSpeed(newSpeed); $scope.eventPlaybackContext.playbackHandlers.setSpeed(newSpeed); $scope.playerShiftPlaybackContext.playbackHandlers.setSpeed(newSpeed); $scope.playerShiftPlaybackContext.speed = newSpeed; mixpanelAPI.send('setSpeed', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId, "speed": newSpeed }, $scope.game )); } $scope.onProgressClick = function (secs) { mixpanelAPI.send('jumpToVideoTime', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId, "videoTime": secs }, $scope.game )); } /** * Method to return the number of players selected by the user * If 'all' is selected, it gets the total number of players in the team */ $scope.getNumberOfPlayersSelected = function(){ var num = 0, homeTeam = $scope.playerSelections.home, homeSetLength = homeTeam.set.length, homeIsAll = homeTeam.isAll, homeRosterLength = (homeTeam && homeTeam.team && homeTeam.team.gameRoster) ? homeTeam.team.gameRoster.length : 0, visitorsTeam = $scope.playerSelections.visitors, visitorsSetLength = visitorsTeam.set.length, visitorsIsAll = visitorsTeam.isAll, visitorsRosterLength = (visitorsTeam && visitorsTeam.team && visitorsTeam.team.gameRoster) ? visitorsTeam.team.gameRoster.length : 0; //add number of players selected num = homeSetLength + visitorsSetLength; //If all is selected, get team roster player numbers and add it up num += homeIsAll ? homeRosterLength : 0; num += visitorsIsAll ? visitorsRosterLength : 0; return num; } $scope.apiEndpoint = '/api/0.1/'; var lastShiftPeriod, vidparams, teamIdSelected, gamePlayerShifts = {}; $scope.currentShift = -1; function playShift(){ var shift = $scope.playerShifts[$scope.currentShift], period = shift.period, shiftStart, shiftDuration; $scope.playerShiftPlaybackContext.showVideoPlaceholder = true; $scope.playerShiftPlaybackContext.currentTargetShift = shift.in; if (!lastShiftPeriod || lastShiftPeriod !== period){ vidparams = $scope.game.periods[period-1].video; } // Find last face-off before current player shift var lastFaceoff; for (var i=0; i < $scope.game.gameEvents.length; i++) { if ($scope.game.gameEvents[i]['name'] == 'faceoff' && $scope.game.gameEvents[i]['period'] == period) { if ($scope.game.gameEvents[i].periodTime > shift.seconds){ lastFaceoffSecondOffset = lastFaceoff.frame / vidparams['frameRate']; break; } lastFaceoff = $scope.game.gameEvents[i]; } } //calculate shift start second in the period video //add 2 second before shift starts and 2 seconds after shifts ends shiftStart = shift.seconds - lastFaceoff.periodTime; shiftStart = (lastFaceoff.frame / vidparams.frameRate) + shiftStart - 2; shiftDuration = shift.duration + 4; // Setup the video in case we changed period or game. // If it's the same, setVideo should do nothing internally $scope.playerShiftPlaybackContext.playbackHandlers.setVideo({ url: { folder: 'https://' + vidparams['rootURL'] + '/', name: vidparams['fileName'] }, frameRate: vidparams['frameRate'] }, shiftStart * vidparams['frameRate']); if (shift){ $scope.playerShiftPlaybackContext.playbackHandlers.seekSecondAndPlayDuration(shiftStart, shiftDuration, $scope.playNextShift); } lastShiftPeriod = period; } $scope.playPreviousShift = function(){ $scope.currentShift--; playShift(); } $scope.playNextShift = function(){ if ($scope.currentShift < $scope.playerShifts.length-1){ $scope.currentShift++; playShift(); } else { return; } } function getPlayerShifts(){ var deferred = $q.defer(), eventsUrl = $scope.apiEndpoint + 'games/' + $scope.game.id + '/playershiftevents', playerShiftData = []; if (!gamePlayerShifts[$scope.game.id]){ gamePlayerShifts[$scope.game.id] = []; $http.get(eventsUrl).then( function(data){ playerShiftData = data.data.events; gamePlayerShifts[$scope.game.id] = playerShiftData; deferred.resolve(gamePlayerShifts[$scope.game.id]); }, function(errors){ deferred.reject(errors); } ); } else { deferred.resolve(gamePlayerShifts[$scope.game.id]); } return deferred.promise; } //PlayerShift Constructor function PlayerShift(lastIn, outEvent){ this.seconds = lastIn.periodTime; this.duration = outEvent.gameTime - lastIn.gameTime; this.secondsOut = this.seconds+this.duration; this.period = lastIn.period; this.event = outEvent; this.in = lastIn; } function createPlayerShifts(gameEvents, playerEvents){ var allEvents = gameEvents.concat(playerEvents), thisPlayerId = playerEvents[0].playerId, i = 0, l = allEvents.length, thisPlayerShift, playerShifts = [], lastIn, hadWhistle; // order events allEvents.sort(function(a, b) { if (a.gameTime != b.gameTime){ return a.gameTime - b.gameTime; } else if (a.frame && b.frame){ return a.frame - b.frame; } else { return a.period - b.period; } }); //loop through all events and check for in, out's, or whistles and faceoffs between an IN and an OUT //shifts are generated if: // Event is 'in' and next event is 'out' // Event is 'in' and next event is 'whistle' // Event is 'faceoff' after a whistle during an 'in', and next event is 'out' // Event is 'faceoff' after a whistle during an 'in', and next event is a 'whistle' again // //shifts that are less than one second and that finishes right after a faceoff are deleted to eliminate timing issues for (; i < l; i++){ var event = angular.copy(allEvents[i]), eventName = event.name, nextEvent = allEvents[i+1], nextEventName = (nextEvent)? nextEvent.name : undefined; //set player id and team id on whistle and faceoff events if (!event.playerId){ event.playerId = thisPlayerId; } if (!event.teamId){ event.teamId = teamIdSelected; } //if player is not currently on ice and event is not an 'in' and presence wasn't interrupted by a whistle if (!lastIn && eventName != 'in' && !hadWhistle){ continue; } //if current presence was interrupted by a whistle and event is a faceoff if (hadWhistle && !lastIn && eventName == 'faceoff'){ lastIn = event; hadWhistle = false; continue; } //if player gets on ice if (!lastIn && eventName === 'in'){ lastIn = event; continue; } //if player presence is interrupted by a whistle //and to prevent timing issues, check if next event is an 'out', if yes, do not end the player shift on the whistle if (lastIn && eventName === 'whistle'){ if (nextEventName !== 'out'){ hadWhistle = true; playerShifts.push(new PlayerShift(lastIn, event, true)); lastIn = undefined; continue; } else { continue; } } //if player shift has ended if (lastIn && eventName === 'out'){ thisPlayerShift = new PlayerShift(lastIn, event); //if this is not a timing issue if (lastIn.name !== 'faceoff' || thisPlayerShift.duration > 1){ playerShifts.push(thisPlayerShift); } hadWhistle = false; lastIn = undefined; continue; } } return playerShifts; } function getSelectedPlayerShifts(allData, selectedPlayers){ var i = 0, l = selectedPlayers.length, selectedPlayersShifts = [], overlappingShifts = [], selectedPeriod = ($scope.selectedPeriod==='all') ? undefined : $scope.selectedPeriod; for (; i < l; i++){ var thisPlayer = selectedPlayers[i], thisPlayerShifts = allData.filter(function( obj ) { if (selectedPeriod){ return obj.playerId === thisPlayer.id && obj.period === selectedPeriod; } else { return obj.playerId === thisPlayer.id; } }); selectedPlayersShifts.push(thisPlayerShifts); } return selectedPlayersShifts; } function getOverlappingPlayerShifts(playerEvents){ var baseShifts = playerEvents[0], i = 0, l = baseShifts.length, baseShiftsTemp = [], ii, ll, iii, lll, shiftsFormatted = []; for (; i < l; i++){ var baseShift = baseShifts[i], overlappedWithPlayers = [], overlap; //loop through other players ii = 1; ll = playerEvents.length; for (; ii < ll; ii++){ var overlappedWithThisPlayer = false; iii = 0; lll = playerEvents[ii].length; //loop through other player events for (; iii < lll; iii++){ var thisShift = playerEvents[ii][iii]; if (thisShift.period === baseShift.period){ var thisShiftStart = thisShift.seconds, thisShiftEnd = thisShift.secondsOut, end = Math.min(baseShift.secondsOut, thisShiftEnd), start = Math.max(baseShift.seconds, thisShiftStart), overlap = Math.max(0, end - start); //if they play together more than 2 seconds... if (overlap > 2) { // console.log('************************************'); // console.log('baseShiftStart:::', baseShift.seconds); // console.log('baseShiftEnd:::', baseShift.secondsOut); // console.log('thisShift.period:::', thisShift.period); // console.log('----'); // console.log('thisShiftStart:::', thisShiftStart); // console.log('thisShiftEnd:::', thisShiftEnd); // console.log('----'); // console.log('start:::', start); // console.log('end:::', end); // console.log('OVERLAP TIME:::', overlap); // console.log('************************************'); var shiftFormatted = { seconds: start, secondsOut: end, duration: overlap, period: thisShift.period, in: { periodTime: thisShiftStart + (start - thisShiftStart), period: thisShift.period } } baseShift = shiftFormatted; overlappedWithThisPlayer = true; } } else { continue; } } overlappedWithPlayers.push(overlappedWithThisPlayer); } if (overlappedWithPlayers.indexOf(false) == -1){ shiftsFormatted.push(baseShift); } } return shiftsFormatted; } function getFormattedPlayersShifts(gameEvents, playerEvents){ var i = 0, l = playerEvents.length, formattedPlayerShifts = []; for (; i < l; i++){ var thisPlayerEvents = playerEvents[i]; if (thisPlayerEvents.length > 0){ formattedPlayerShifts.push(createPlayerShifts(gameEvents, thisPlayerEvents)); } } return formattedPlayerShifts; } $scope.viewPlayerShiftVideos = function(){ var homePlayers = $scope.playerSelections.home.set, visitorPlayers = $scope.playerSelections.visitors.set, selectedPlayers = homePlayers.concat(visitorPlayers), selectedPeriod = ($scope.selectedPeriod==='all') ? undefined : $scope.selectedPeriod, faceoffEvents, whistleEvents, gameEvents, allEvents; $scope.selectedPlayers = selectedPlayers; //copy to avoid keeping the reference to the scope variable as we modify the new variable later and don't want to modify the scope variable faceoffEvents = angular.copy($filter('filter')($scope.game.gameEvents, {"name": "faceoff"})); whistleEvents = angular.copy($filter('filter')($scope.game.gameEvents, {"name": "whistle"})); gameEvents = angular.copy(whistleEvents.concat(faceoffEvents)); if (selectedPeriod){ gameEvents = $filter('filter')(gameEvents, {"period": selectedPeriod}); } //on button click, reset values $scope.currentShift = -1; lastShiftPeriod = undefined; vidparams = undefined; teamIdSelected = undefined; getPlayerShifts().then( function(data){ var currentPlayersEvents = [], playersShiftsFormatted = []; currentPlayersEvents = getSelectedPlayerShifts(data, selectedPlayers); playersShiftsFormatted = getFormattedPlayersShifts(gameEvents, currentPlayersEvents); if (playersShiftsFormatted.length > 0 && playersShiftsFormatted.length === selectedPlayers.length){ overlappingPlayerShiftsFormatted = getOverlappingPlayerShifts(playersShiftsFormatted); if (overlappingPlayerShiftsFormatted.length > 0) { $scope.playerShifts = overlappingPlayerShiftsFormatted; //show video player if ($scope.playerShiftPlaybackContext.isVideoPlayerActive == false) { $scope.playerShiftPlaybackContext.isVideoPlayerActive = true; } mixpanelAPI.send('playbackPlayerShifts', mixpanelAPI.decorateDetailsWithGameInfo({ "view": $scope.slId, "numberOfShifts": $scope.playerShifts.length, "homePlayersSelection": $filter('formatPlayerSelection')($scope.playerSelections['home']), "awayPlayersSelection": $filter('formatPlayerSelection')($scope.playerSelections['visitors']), "periodEventFilter": $scope.selectedPeriodSet, "manpowerSituationFilter": $scope.manpowerSituations }, $scope.game )); //play the first shift on load $scope.playNextShift(); } else { var status = 200, message = 'Could not find any overlapping player shifts for the players selected'; errorStatusCheck.checkErrorStatus(data,status,message); } } else { var status = 200, message = 'Could not find player shifts for all the players selected'; errorStatusCheck.checkErrorStatus(data,status,message); } }, function(data){ var status = data.status, message='Could not get player shift events for gameId ' + $scope.game.id; errorStatusCheck.checkErrorStatus(data,status,message); } ); } } // end of controller }; // end of directoive factory });