app/js/controllers/search.js
'use strict';
/* global angular, L, map */
/* jshint unused:false */
var controllersModule = require('./_index');
var PhotoSwipe = require('photoswipe');
var PhotoSwipeUI_Default = require('../components/photoswipe-ui-default');
/**
* @ngInject
*/
controllersModule.controller('SearchCtrl', ['$window', '$stateParams', '$rootScope', '$scope', '$timeout', '$location', '$filter', '$interval', 'SearchService', 'DebugLinkService', 'MapPopUpTemplateService', 'MapCreationService' , function($window, $stateParams, $rootScope, $scope, $timeout, $location, $filter, $interval, SearchService, DebugLinkService, MapPopUpTemplateService, MapCreationService) {
// Define models here
var intervals = [];
var vm = this;
var prevZoomAction, prevPanAction, newZoomAction, newPanAction;
vm.modal = { text: "Tweet content placeholder"};
vm.showDetail = false;
vm.showResult = false;
vm.term = '';
vm.pool = [];
vm.statuses = [];
vm.showingResultInAcc = 9;
$rootScope.root.globalFilter = '';
function getMoreStatuses() {
if (vm.pool.length > 0) {
// Get new time span bound from the lastest status
var currentUntilBound = new Date(vm.pool[vm.pool.length -1 ].created_at);
var newUntilBound = new Date(currentUntilBound.getTime() - 1);
var untilSearchParam = $filter("date")(newUntilBound, "yyyy-MM-dd_HH:mm");
var newSearchParam = $location.search().q + "+until:" + untilSearchParam;
var params = {
q: newSearchParam,
timezoneOffset: (new Date().getTimezoneOffset())
};
// Get new data and concat to the current result
SearchService.initData(params).then(function(data) {
vm.pool = vm.pool.concat(data.statuses);
}, function() {});
}
}
/*
* Infinity scroll directive's trigger
* trigger arg: @amount
* An @amount of statuses will be concatenated to the current result
* If the pool of statuses's level is low, get more statuses
* Is also used to init the shown result
*/
$scope.loadMore = function(amount) {
if (!vm.peopleSearch) {
if (vm.pool.length < (2 * amount + 1)) {
getMoreStatuses();
}
vm.statuses = vm.statuses.concat(vm.pool.slice(0,amount));
vm.pool = vm.pool.splice(amount);
} else {
vm.showingResultInAcc += amount;
}
};
/*
* When a new search is made or the outgoing search is terminated
* $rootScope.cancelPromise is defined when a search is made from SearchService
*/
var cancelAllRequest = function() {
if ($rootScope.httpCanceler) {
angular.forEach(intervals, function(interval) {
$interval.cancel(interval);
});
$rootScope.httpCanceler.resolve();
}
};
/*
* Helper fn for filters, including
* A method to update path based on current search term & filter
* A method to convert current path params => search term & filter
* A method for vice versa
*/
function updatePath(query) {
$location.search({q: query});
$rootScope.root.globalSearchTerm = vm.term;
}
function filterToQuery(filterName) {
var filtersToQueries = {
'photos' : '/image',
'videos' :'/video',
'accounts' : '/accounts',
'news' : '/news',
'map' : '/map'
};
return filtersToQueries[filterName];
}
/*
* Background process to get newest result, including
* A fn to get more result, compare created_at and update statuses's pool
* A method defintion to clear and start a new interval
* A ng-click trigger to load newest statuses
*/
var getManuallyNewestStatuses = function() {
if (vm.statuses[0]) {
var lastestDateObj = new Date(vm.statuses[0].created_at);
var term = ($rootScope.root.globalFilter === 'live') ? vm.term : vm.term + '+' + filterToQuery($rootScope.root.globalFilter);
SearchService.getData(term).then(function(data) {
var keepComparing = true; var i = 0;
while (keepComparing) {
if (new Date(data.statuses[i].created_at) <= lastestDateObj) {
vm.newStasuses = data.statuses.slice(0, i);
vm.noOfNewStatuses = vm.newStasuses.length;
keepComparing = false;
}
i++;
}
}, function() {});
}
};
var startNewInterval = function(period) {
angular.forEach(intervals, function(interval) {
$interval.cancel(interval);
});
intervals.push($interval(getManuallyNewestStatuses, parseInt(period, 10)));
};
/*
* Wrapper for SearchService, including
* Updating path operation before search
* Init result with 20 statuses
* Init a background interval to update the result
*/
vm.update = function(term) {
cancelAllRequest();
updatePath(term);
SearchService.getData(term).then(function(data) {
vm.pool = data.statuses;
vm.statuses = [];
$scope.loadMore(20);
vm.showResult = true;
startNewInterval(data.search_metadata.period);
}, function() {});
};
////////
//// FILTERS FEATURE
//// filter fn is used as ng-click trigger
//// a typical filter trigger will include these operation:
//// - change current filter model; - show/hide related result according to filter
//// - request for new result; - update path based on search term & filter
//// Filter explanation: live = no filter; accounts = show accounts only;
//// photos = tweet with native twitter photo + tweet with recognized photo link
//// video = tweet with native twitter video + tweet with recognized video link
//// map = no filter, but results are shown on a map
////////
vm.filterLive = function() {
$rootScope.root.globalFilter = 'live';
vm.newStasuses = [];
vm.peopleSearch = false;
vm.showMap = false;
var term = vm.term;
vm.update(term);
};
vm.filterAccounts = function() {
cancelAllRequest();
$rootScope.root.globalFilter = 'accounts';
vm.newStasuses = [];
vm.accountsPretty = [];
vm.accounts = [];
var term = vm.term;
SearchService.initData({q: term, count: 200}).then(function(data) {
// Get screen_name, then remove duplicates
data.statuses.forEach(function(ele) { vm.accounts.push(ele.screen_name); });
vm.accounts = vm.accounts.filter(function(item, pos) { return vm.accounts.indexOf(item) === pos; });
SearchService.retrieveMultipleImg(vm.accounts).then(function(data) {
vm.accountsPretty = data.users;
vm.peopleSearch = true;
vm.showMap = false;
}, function() {});
vm.accountsPretty.forEach(function(ele) {
var result = $filter('filter')($rootScope.userTopology.followers, {screen_name : ele.screen_name}, true);
if(result.length) {
ele.friend = true;
}
else ele.friend = false;
});
}, function() {});
updatePath(vm.term + '+' + '/accounts');
};
vm.filterPhotos = function() {
$rootScope.root.globalFilter = 'photos';
vm.newStasuses = [];
vm.statuses = [];
vm.peopleSearch = false;
vm.showMap = false;
var term = vm.term + '+' + '/image';
vm.update(term);
};
vm.filterVideos = function() {
cancelAllRequest();
$rootScope.root.globalFilter = 'videos';
vm.statuses = [];
vm.newStasuses = [];
vm.peopleSearch = false;
vm.showMap = false;
vm.showResult = true;
// Tweet with native video has a value in this.videos array
// Move that value to this.links to be evaluated by debugged-link directive
SearchService.getData(vm.term + '+' + '/video').then(function(data) {
var statuses = data.statuses;
var statusesWithVideo = [];
statuses.forEach(function(status) {
if (status.videos_count) {
if (status.videos[0].substr(-4) === '.mp4') {
status.links[0] = status.videos[0];
}
statusesWithVideo.push(status);
}
});
vm.pool = statusesWithVideo;
vm.statuses = [];
$scope.loadMore(15);
startNewInterval(data.search_metadata.period);
}, function() {});
updatePath(vm.term + '+' + '/video');
};
/*
* Add listener on maps' action
* When zoom/pan, new /location bound is calculated, and then is used to get & add more map points
* prevZoomAction, prevPanAction, newZoomAction, newPanAction are used to prevent event bubbling
*/
function getMoreLocationOnMapAction() {
var bound = window.map.getBounds();
var locationTerm = MapCreationService.getLocationParamFromBound(bound);
var params = { q: vm.term + "+" + locationTerm, count: 300};
SearchService.initData(params).then(function(data) {
MapCreationService.addPointsToMap(window.map, MapCreationService.initMapPoints(data.statuses, "genStaticTwitterStatus"), "simpleCircle", addListenersOnMap);
}, function(data) {});
}
function addListenersOnMap() {
window.map.on("zoomend", function(event) {
if (!prevZoomAction) {
prevZoomAction = new Date();
getMoreLocationOnMapAction();
} else {
newZoomAction = new Date();
var interval = (newZoomAction - prevZoomAction);
if (interval > 1000) {
getMoreLocationOnMapAction();
prevZoomAction = newZoomAction;
}
}
});
window.map.on("dragend", function(event) {
if (!prevPanAction) {
prevPanAction = new Date();
getMoreLocationOnMapAction();
} else {
newPanAction = new Date();
var interval = (newPanAction - prevPanAction);
if (interval > 1000) {
getMoreLocationOnMapAction();
prevPanAction = newPanAction;
}
}
});
}
vm.filterMap = function() {
cancelAllRequest();
if (window.map) { delete(window.map); }
vm.newStasuses = [];
$rootScope.root.globalFilter = "map";
vm.statuses = [];
vm.accounts = [];
vm.showMap = true;
updatePath(vm.term + '+' + '/map');
var initialBound = "/location=-282.65625,-77.54209596075546,307.96875,86.40197606876063";
var params = { q: vm.term + "+" + initialBound, count: 300 };
SearchService.initData(params).then(function(data) {
var withoutLocation = [];
data.statuses.forEach(function(ele, index) {
if (!ele.location_mark) {
withoutLocation.push(data.statuses.splice(index, 1)[0]);
}
});
MapCreationService.initMap({
data: data.statuses,
mapId: "search-map",
markerType: "simpleCircle",
templateEngine: "genStaticTwitterStatus",
cbOnMapAction: addListenersOnMap
});
MapCreationService.addLocationFromUser(withoutLocation);
}, function() {});
angular.forEach(intervals, function(interval) {
$interval.cancel(interval);
});
};
function evalSearchQuery() {
var queryParts = $location.search().q.split('+');
var queryToFilter = {
'/image': 'photos',
'/video': 'videos',
'/accounts': 'accounts',
'/news' : 'news',
'/map' : 'map'
};
vm.term = queryParts[0];
$rootScope.root.globalFilter = (queryParts[1]) ? queryToFilter[queryParts[1]] : 'live';
}
function scrapeImgTag(imgTag) {
var ngEle = angular.element(imgTag);
return {
src: ngEle.attr('src'),
w: parseInt(ngEle.css('width').replace('px', ''), 10),
h: parseInt(ngEle.css('height').replace('px', ''), 10)
};
}
/*
* Status's dỉrective openSwipe fn
* Clicking on an image results in a photoswipe
* Docs: http://photoswipe.com/documentation/getting-started.html
*/
vm.openSwipe = function(status_id) {
var items = [];
var images = angular.element('#' + status_id + ' .images-wrapper img');
var options = { index: 0, history: false};
var swipeEle = document.querySelectorAll('.pswp')[0];
var swipeObject = 'gallery' + status_id;
angular.forEach(images, function(image) {
this.push(scrapeImgTag(image));
}, items);
$timeout(function() {
window[swipeObject] = new PhotoSwipe(swipeEle, PhotoSwipeUI_Default, items, options);
window[swipeObject].init();
}, 0);
};
vm.showNewStatuses = function() {
vm.statuses = vm.newStasuses.concat(vm.statuses);
vm.newStasuses = [];
};
////////////
// MANAGING STATE CHANGES RESULTING IN SEARCH
///////////
// On search params in path change
$scope.$watch(function() {
return $location.search();
}, function(value, Oldvalue) {
if (value.q && value.q.indexOf("id:") > -1) { // When q has "id=.." Leave this for single-tweet view
return 1;
}
if (value.q && value.q.indexOf("from:") > -1) { // When q has "id=.." Leave this for single-tweet view
var screen_name = value.q.slice(5); //Get screen_name only
$location.url("/topology?screen_name=" + encodeURIComponent(screen_name));
return 1;
}
if (value.q.split("+")[0] !== vm.term) { // Else evaluate path and start search operation
evalSearchQuery();
var filterFn = 'filter' + $filter('capitalize')($rootScope.root.globalFilter);
vm[filterFn]();
vm.showResult = true;
}
// Edge case: when click on map tweet, but search term stay the same
// e.g. search for @codinghorror+/map, and then clicking for @codinghorror on the map
if (value.q.split("+")[0] === Oldvalue.q.split("+")[0] && (Oldvalue.q.split("+")[1] && value.q.split("+")[1] === undefined)) {
evalSearchQuery();
SearchService.getData(vm.term).then(function(data) {
vm.pool = data.statuses;
vm.statuses = [];
$scope.loadMore(20);
vm.showResult = true;
startNewInterval(data.search_metadata.period);
}, function() {});
$rootScope.root.globalSearchTerm = vm.term;
vm.showMap = false;
vm.peopleSearch = false;
vm.showResult = true;
}
});
////////////
// MANAGING STATE OF FAILURE REQUESTS
///////////
$rootScope.$watch(function() {
return $rootScope.root.numberOfFailedReq;
}, function(val) {
if (val >= 2) {
$window.location.reload();
}
});
}]);