diff options
Diffstat (limited to 'www/js')
30 files changed, 22461 insertions, 24 deletions
diff --git a/www/js/DataModel.js b/www/js/DataModel.js new file mode 100755 index 00000000..8f702c66 --- /dev/null +++ b/www/js/DataModel.js @@ -0,0 +1,2193 @@ +/* jshint -W041 */ + +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console, URI, moment, localforage, CryptoJS, Connection */ + +// This is my central data respository and common functions +// that many other controllers use +// It's grown over time. I guess I may have to split this into multiple services in the future + +angular.module('zmApp.controllers') + +.service('NVRDataModel', ['$http', '$q', '$ionicLoading', '$ionicBackdrop', '$fileLogger', 'zm', '$rootScope', '$ionicContentBanner', '$timeout', '$cordovaPinDialog', '$ionicPopup', '$localstorage', '$state', '$ionicNativeTransitions', '$translate', '$cordovaSQLite', + function($http, $q, $ionicLoading, $ionicBackdrop, $fileLogger, + zm, $rootScope, $ionicContentBanner, $timeout, $cordovaPinDialog, + $ionicPopup, $localstorage, $state, $ionicNativeTransitions, $translate) + { + + var zmAppVersion = "unknown"; + var isBackground = false; + var justResumed = false; + var monitorsLoaded = 0; + //var montageSize = 3; + var monitors = []; + var multiservers = []; + var oldevents = []; + var migrationComplete = false; + + var tz = ""; + var isTzSupported = false; + + var languages = [ + { + text: 'English', + value: 'en' + }, + { + text: 'العربية', + value: 'ar' + }, + { + text: 'Deutsch', + value: 'de' + }, + { + text: 'Español', + value: 'es' + }, + + { + text: 'Français', + value: 'fr' + }, + + { + text: 'Italiano', + value: 'it' + }, + { + text:'Magyar', + value:'hu' + }, + { + text: 'Nederlands', + value: 'nl' + }, + { + text: 'Polski', + value: 'pl' + }, + { + text: 'Portugese', + value: 'pt' + }, + { + text: 'Русский', + value: 'ru' + }, + + + + /* { + text: 'Hindi', + value: 'hi' + }*/ + ]; + + var serverGroupList = {}; + var defaultLang = 'en'; + var isFirstUse = true; + var lastUpdateCheck = null; + var latestBlogPostChecked = null; + var loginData = { + 'serverName': '', + 'username': '', + 'password': '', + 'fallbackConfiguration': '', + 'url': '', // This is the ZM portal path + 'apiurl': '', // This is the API path + 'eventServer': '', //experimental Event server address + 'maxMontage': "100", //total # of monitors to display in montage + 'streamingurl': "", + 'maxFPS': "3", // image streaming FPS + 'montageQuality': "50", // montage streaming quality in % + 'singleImageQuality': "100", // event single streaming quality in % + 'monSingleImageQuality': "100", // live view quality + 'montageHistoryQuality': "50", + 'useSSL': false, // "1" if HTTPS + 'keepAwake': true, // don't dim/dim during live view + 'isUseAuth': true, // true if user wants ZM auth + 'isUseEventServer': false, // true if you configure the websocket event server + 'disablePush': false, // true if only websocket mode is desired + 'eventServerMonitors': '', // list of monitors to notify from ES + 'eventServerInterval': '', // list of intervals for all monitors + 'refreshSec': '2', // timer value for frame change in sec + 'refreshSecLowBW': 8, + 'enableLogs': true, + 'enableDebug': true, // if enabled with log messages with "debug" + 'usePin': false, + 'pinCode': '', + 'canSwipeMonitors': true, + 'persistMontageOrder': false, + 'onTapScreen': "", + 'enableh264': true, + 'gapless': false, + 'montageOrder': '', + 'montageHiddenOrder': '', + 'montageArraySize': '0', + 'showMontageSubMenu': false, + 'graphSize': 2000, + 'enableAlarmCount': true, + 'minAlarmCount': 1, + 'montageSize': '3', + 'useNphZms': true, + 'useNphZmsForEvents': true, + 'packMontage': false, + 'exitOnSleep': false, + 'forceNetworkStop': false, + 'defaultPushSound': false, + 'enableBlog': true, + 'use24hr': false, + 'packeryPositions': '', + 'currentMontageProfile': '', + 'packeryPositionsArray': {}, + 'EHpackeryPositions': '', + 'packerySizes': '', + 'timelineModalGraphType': 'all', + 'resumeDelay': 0, + 'language': 'en', + 'reachability': true, + 'forceImageModePath': false, + 'disableNative': false, + 'vibrateOnPush': true, + 'soundOnPush': true, + 'cycleMonitors': false, + 'cycleMontage': false, + 'cycleMontageInterval': 10, // 10sec + 'cycleMonitorsInterval': 10, // 10sec + 'enableLowBandwidth': false, + 'autoSwitchBandwidth': false, + 'disableAlarmCheckMontage': false, + 'useLocalTimeZone': true, + 'fastLogin': true, + 'followTimeLine': false, + 'timelineScale': -1, + 'hideArchived': false, + 'videoPlaybackSpeed': 2, + 'enableGIFMP4': false, + 'enableStrictSSL': false, + 'enableSlowLoading': false, + + }; + + var defaultLoginData = angular.copy(loginData); + + var configParams = { + 'ZM_EVENT_IMAGE_DIGITS': '-1', + 'ZM_PATH_ZMS': '' + }; + + + function setSSLCerts() + { + if (!window.cordova) return; + if (!loginData.enableStrictSSL) + { + + //alert("Enabling insecure SSL"); + log(">>>> Disabling strict SSL checking (turn off in Dev Options if you can't connect)"); + cordova.plugins.certificates.trustUnsecureCerts(true); + + } + else + { + + log(">>>> Enabling strict SSL checking (turn off in Dev Options if you can't connect)"); + cordova.plugins.certificates.trustUnsecureCerts(false); + } + } + + + // credit: http://stackoverflow.com/questions/4994201/is-object-empty + function isEmpty(obj) + { + + // null and undefined are "empty" + if (obj == null) return true; + + // Assume if it has a length property with a non-zero value + // that that property is correct. + if (obj.length > 0) return false; + if (obj.length === 0) return true; + + // Otherwise, does it have any properties of its own? + // Note that this doesn't handle + // toString and valueOf enumeration bugs in IE < 9 + for (var key in obj) + { + if (hasOwnProperty.call(obj, key)) return false; + } + + return true; + } + + function getBandwidth() + { + // if mode is not on always return high + if (loginData.enableLowBandwidth == false) + { + return "highbw"; + } + // if mode is force on, return low + if (loginData.enableLowBandwidth == true && loginData.autoSwitchBandwidth != true) + { + return "lowbw"; + } + if (loginData.enableLowBandwidth == true && loginData.autoSwitchBandwidth == true && $rootScope.platformOS == 'desktop') + { + return "highbw"; + } + // else return real state + + var networkState = navigator.connection.type; + var strState; + switch (networkState) + { + + case Connection.WIFI: + strState = "highbw"; + break; + case Connection.ETHERNET: + strState = "highbw"; + break; + default: + strState = "lowbw"; + break; + + } + return strState; + } + + //-------------------------------------------------------------------------- + // uses fileLogger to write logs to file for later investigation + //-------------------------------------------------------------------------- + + // separate out a debug so we don't do this if comparison for normal logs + function debug(val) + { + if (loginData.enableDebug && loginData.enableLogs) + { + if (val !== undefined) + { + var regex1 = /"password":".*?"/g; + var regex2 = /&pass=.*?(?=["&]|$)/g; + + //console.log ("VAL IS " + val); + val = val.replace(regex1, "<password removed>"); + val = val.replace(regex2, "<password removed>"); + } + $fileLogger.debug(val); + //console.log (val); + } + } + + function log(val, logtype) + { + if (loginData.enableLogs) + { + if (val !== undefined) + { + var regex1 = /"password":".*?"/g; + var regex2 = /&pass=.*?(?=["&]|$)/g; + + //console.log ("VAL IS " + val); + val = val.replace(regex1, "<password removed>"); + val = val.replace(regex2, "<password removed>"); + + } + // make sure password is removed + //"username":"zmninja","password":"xyz", + //val = val.replace(/\"password:\", + $fileLogger.log(logtype, val); + // console.log (val); + } + } + + function reloadMonitorDisplayStatus() + { + debug("Loading hidden/unhidden status for profile:"+loginData.currentMontageProfile); + + var positionsStr = loginData.packeryPositions; + //console.log ("positionStr="+positionsStr); + var positions = {}; + if (loginData.packeryPositions != '' && loginData.packeryPositions != undefined) + { + console.log ("positions="+loginData.packeryPositions); + + + positions = JSON.parse(positionsStr); + for (var m = 0; m < monitors.length; m++) + { + var positionFound = false; + for (var p = 0; p < positions.length; p++) + { + if (monitors[m].Monitor.Id == positions[p].attr) + { + monitors[m].Monitor.listDisplay = positions[p].display; + positionFound = true; + debug("DataModel: Setting MID:" + monitors[m].Monitor.Id + " to " + monitors[m].Monitor.listDisplay); + } + + } + if (!positionFound) + { + if (loginData.currentMontageProfile != $translate.instant('kMontageDefaultProfile')) + { + monitors[m].Monitor.listDisplay = 'noshow'; + console.log("*************DISABLE NEW MONITOR"); + } + else // make sure we add it because its show all view + { + monitors[m].Monitor.listDisplay = 'show'; + console.log("*************ENABLE NEW MONITOR"); + } + + + } + + } + + } + else // if there are no packery positions, make sure all are displayed! + { + debug ("no packery profile, making sure monitors are show"); + for (var m1 = 0; m1 < monitors.length; m1++) + { + monitors[m1].Monitor.listDisplay = 'show'; + + } + + + } + } + + function setLogin(newLogin) + { + loginData = angular.copy(newLogin); + serverGroupList[loginData.serverName] = angular.copy(loginData); + + var ct = CryptoJS.AES.encrypt(JSON.stringify(serverGroupList), zm.cipherKey).toString(); + + //console.log ("****serverLogin was encrypted to " + ct); + //$localstorage.setObject("serverGroupList", serverGroupList); + localforage.setItem("serverGroupList", ct, function(err) + { + if (err) log("localforage store error " + JSON.stringify(err)); + }); + //$localstorage.set("defaultServerName", loginData.serverName); + localforage.setItem("defaultServerName", loginData.serverName, function(err) + { + if (err) log("localforage store error " + JSON.stringify(err)); + }); + + } + + //credit: https://gist.github.com/alexey-bass/1115557 + function versionCompare(left, right) + { + if (typeof left + typeof right != 'stringstring') + return false; + + var a = left.split('.'); + var b = right.split('.'); + var i = 0; + var len = Math.max(a.length, b.length); + + for (; i < len; i++) + { + if ((a[i] && !b[i] && parseInt(a[i]) > 0) || (parseInt(a[i]) > parseInt(b[i]))) + { + return 1; + } + else if ((b[i] && !a[i] && parseInt(b[i]) > 0) || (parseInt(a[i]) < parseInt(b[i]))) + { + return -1; + } + } + + return 0; + } + + //-------------------------------------------------------------------------- + // Banner display of messages + //-------------------------------------------------------------------------- + function displayBanner(mytype, mytext, myinterval, mytimer) + { + + var contentBannerInstance = + $ionicContentBanner.show( + { + text: mytext || 'no text', + interval: myinterval || 2000, + //autoClose: mytimer || 6000, + type: mytype || 'info', + transition: 'vertical', + //cancelOnStateChange: false + }); + + $timeout(function() + { + contentBannerInstance(); + }, mytimer || 6000); + } + + return { + + //------------------------------------------------------------- + // used by various controllers to log messages to file + //------------------------------------------------------------- + + migrationComplete: function() + { + migrationComplete = true; + }, + + isEmpty: function(obj) + { + return isEmpty(obj); + }, + + log: function(val, type) + { + var logtype = 'info'; + if (type != undefined) + logtype = type; + log(val, logtype); + + }, + + debug: function(val) + { + + debug(val); + }, + + setLastUpdateCheck: function(val) + { + lastUpdateCheck = val; + localforage.setItem("lastUpdateCheck", lastUpdateCheck); + }, + + getLastUpdateCheck: function() + { + return lastUpdateCheck; + }, + + setLatestBlogPostChecked: function(val) + { + console.log (">>>>>>>>>>>> Setting blog date: " + val); + latestBlogPostChecked = val; + localforage.setItem("latestBlogPostChecked", latestBlogPostChecked); + }, + + getLatestBlogPostChecked: function() + { + return latestBlogPostChecked; + }, + + // This function is called when the app is ready to run + // sets up various variables + // including persistent login data for the ZM apis and portal + // The reason I need both is because as of today, there is no way + // to access images using the API and they are authenticated via + // the ZM portal authentication, which is pretty messy. But unless + // the ZM authors fix this and streamline the access of images + // from APIs, I don't have an option + + zmStateGo: function(state, p1, p2) + { + if ($rootScope.platformOS == 'desktop') + $state.go(state, p1, p2); + else + $ionicNativeTransitions.stateGo(state, p1, p2); + }, + + // used when an empty server profile is created + getDefaultLoginObject: function() + { + return angular.copy(defaultLoginData); + }, + + getReachableConfig: function(skipFirst) + { + var d = $q.defer(); + if (loginData.serverName == "") + { + log("Reachable: No server name configured, likely first use?"); + d.reject("No servers"); + return d.promise; + } + + var chainURLs = []; + var savedLoginData = angular.copy(loginData); + + //log ("Making sure " + loginData.serverName + " is reachable..."); + var tLd = serverGroupList[loginData.serverName]; + if (skipFirst && tLd.fallbackConfiguration) + { + tLd = serverGroupList[tLd.fallbackConfiguration]; + if (!tLd) + { + d.reject("No available severs"); + loginData = savedLoginData; + return d.promise; + + } + } + + var keepBuilding = true; + while (keepBuilding == true && tLd) + { + if (arrayObjectIndexOf(chainURLs, tLd.url + "/index.php", "url") == -1 && tLd.url !== undefined && tLd.url != '') // no loop + { + log("Adding to chain stack: " + tLd.serverName + ">" + tLd.url); + chainURLs.push( + { + url: tLd.url + "/index.php", + server: tLd.serverName + }); + log("Fallback of " + tLd.serverName + " is " + tLd.fallbackConfiguration); + if (tLd.fallbackConfiguration) + { + tLd = serverGroupList[tLd.fallbackConfiguration]; + if (tLd === undefined) + { + // This can happen if the fallback profile was deleted + log("Looks like a server object was deleted, but is still in fallback"); + keepBuilding = false; + } + } + else + { + log("reached end of chain loop"); + } + } + else + { + log("detected loop when " + tLd.serverName + " fallsback to " + tLd.fallbackConfiguration); + keepBuilding = false; + } + } + + //contactedServers.push(loginData.serverName); + findFirstReachableUrl(chainURLs).then(function(firstReachableUrl) + { + d.resolve(firstReachableUrl); + // also make sure loginData points to this now + + loginData = angular.copy(serverGroupList[firstReachableUrl.server]); + + setLogin(loginData); + //$localstorage.set("defaultServerName",firstReachableUrl.server); + + log("Based on reachability, first serverName will be " + firstReachableUrl.server); + //console.log("set login Data to " + JSON.stringify(loginData)); + + return d.promise; + // OK: do something with firstReachableUrl + }, function() + { + d.reject("No servers reachable"); + loginData = savedLoginData; + return d.promise; + // KO: no url could be reached + }); + + function arrayObjectIndexOf(myArray, searchTerm, property) + { + for (var i = 0, len = myArray.length; i < len; i++) + { + if (myArray[i][property] === searchTerm) + return i; + } + return -1; + } + + function findFirstReachableUrl(urls) + { + if (urls.length > 0 && $rootScope.userCancelledAuth != true) + { + $ionicLoading.show( + { + template: $translate.instant('kTrying') + ' ' + urls[0].server + }); + log("Reachability test.." + urls[0].url); + + if (loginData.reachability) + { + + //console.log ("************* AUGH"); + var hDelay = loginData.enableSlowLoading? zm.largeHttpTimeout:zm.httpTimeout; + return $http({method:'GET', timeout:hDelay, url:urls[0].url}).then(function() + { + log("Success: reachability on " + urls[0].url); + $ionicLoading.hide(); + return urls[0]; + }, function(err) + { + log("Failed reachability on " + urls[0].url + " with error " + JSON.stringify(err)); + return findFirstReachableUrl(urls.slice(1)); + }); + } + else + { + log("Reachability is disabled in config, faking this test and returning success on " + urls[0]); + return urls[0]; + } + } + else + { + $ionicLoading.hide(); + return $q.reject("No reachable URL"); + + } + + } + + return d.promise; + + }, + + init: function() + { + // console.log("****** DATAMODEL INIT SERVICE CALLED ********"); + + log("ZMData init: checking for stored variables & setting up log file"); + + localforage.getItem("latestBlogPostChecked") + .then (function (val) {latestBlogPostChecked = val;}, + function (err) {latestBlogPostChecked = null;}); + + + $ionicLoading.show( + { + template: $translate.instant('kRetrievingProfileData'), + }); + + localforage.getItem("serverGroupList").then(function(val) + { + // decrypt it now + + var decodedVal; + + if (typeof val == 'string') + { + log("user profile encrypted, decoding..."); + var bytes = CryptoJS.AES.decrypt(val.toString(), zm.cipherKey); + decodedVal = JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); + + } + else + { + log("user profile not encrypted"); + decodedVal = val; + } + + //decodedVal = val; + + // debug("user profile retrieved:" + JSON.stringify(decodedVal)); + + $ionicLoading.hide(); + serverGroupList = decodedVal; + + // console.log(">>>> DECRYPTED serverGroupList " + JSON.stringify(serverGroupList)); + var demoServer = "{\"serverName\":\"zmNinjaDemo\",\"username\":\"zmninja\",\"password\":\"zmNinja$xc129\",\"url\":\"https://demo.zoneminder.com/zm\",\"apiurl\":\"https://demo.zoneminder.com/zm/api\",\"eventServer\":\"\",\"maxMontage\":\"40\",\"streamingurl\":\"https://demo.zoneminder.com/cgi-bin-zm\",\"maxFPS\":\"3\",\"montageQuality\":\"50\",\"singleImageQuality\":\"100\",\"montageHistoryQuality\":\"50\",\"useSSL\":true,\"keepAwake\":true,\"isUseAuth\":\"1\",\"isUseEventServer\":false,\"disablePush\":false,\"eventServerMonitors\":\"\",\"eventServerInterval\":\"\",\"refreshSec\":\"2\",\"enableDebug\":false,\"usePin\":false,\"pinCode\":\"\",\"canSwipeMonitors\":true,\"persistMontageOrder\":false,\"onTapScreen\":\"Events\",\"enableh264\":true,\"gapless\":false,\"montageOrder\":\"\",\"montageHiddenOrder\":\"\",\"montageArraySize\":\"0\",\"graphSize\":2000,\"enableAlarmCount\":true,\"montageSize\":\"3\",\"useNphZms\":true,\"useNphZmsForEvents\":true,\"packMontage\":false,\"exitOnSleep\":false,\"forceNetworkStop\":false,\"defaultPushSound\":false,\"enableBlog\":true,\"use24hr\":false, \"packeryPositions\":\"\"}"; + var demoS = JSON.parse(demoServer); + //console.log("JSON parsed demo" + JSON.stringify(demoS)); + + var isFoundDemo = false; + var as = Object.keys(serverGroupList); + for (var x = 0; x < as.length; x++) + { + if (as[x] == 'zmNinjaDemo') + isFoundDemo = true; + //console.log ("************ FOUND SERVER NAME " + as[x]); + // if serverGroupList[x] + } + + // Don't add the demo if there is another server + // because this means the user deleted it + + if (!isFoundDemo && as.length == 0) + { + debug("Pushing demo server config to server groups"); + //serverGroupList.push(demoS); + serverGroupList[demoS.serverName] = angular.copy(demoS); + } + + var sname; + $ionicLoading.show( + { + template: $translate.instant('kRetrievingProfileData'), + }); + localforage.getItem("defaultServerName") + .then(function(val) + { + $ionicLoading.hide(); + //console.log ("!!!!!!!!!!!!!!!!!!default server name is " + sname); + sname = val; + // console.log("!!!!!!!!!!!!!!!!!!!Got VAL " + sname); + var loadedData = serverGroupList[sname]; + // console.log(">>>>>>>>>>> loadedData is: " + JSON.stringify(loadedData)); + if (!isEmpty(loadedData)) + { + loginData = loadedData; + + // old version hacks for new variables + + // always true Oct 27 2016 + loginData.persistMontageOrder = true; + loginData.enableh264 = true; + + if (typeof loginData.enableAlarmCount === 'undefined') + { + debug("enableAlarmCount does not exist, setting to true"); + loginData.enableAlarmCount = true; + } + + if (typeof loginData.onTapScreen == 'undefined') + { + loginData.onTapScreen = $translate.instant('kTapMontage'); + } + + if (loginData.onTapScreen != $translate.instant('kTapMontage') && + loginData.onTapScreen != $translate.instant('kTapEvents') && + loginData.onTapScreen != $translate.instant('kTapLiveMonitor')) + { + log("Invalid onTap setting found, resetting"); + loginData.onTapScreen = $translate.instant('kMontage'); + } + + if (typeof loginData.minAlarmCount === 'undefined') + { + debug("minAlarmCount does not exist, setting to true"); + loginData.minAlarmCount = 1; + } + + if (typeof loginData.montageSize == 'undefined') + { + debug("montageSize does not exist, setting to 2 (2 per col)"); + loginData.montageSize = 2; + } + + if (typeof loginData.useNphZms == 'undefined') + { + debug("useNphZms does not exist. Setting to true"); + loginData.useNphZms = true; + } + + if (typeof loginData.useNphZmsForEvents == 'undefined') + { + debug("useNphZmsForEvents does not exist. Setting to true"); + loginData.useNphZmsForEvents = true; + } + + if (typeof loginData.forceImageModePath == 'undefined') + { + debug("forceImageModePath does not exist. Setting to false"); + loginData.forceImageModePath = false; + } + + if (typeof loginData.reachability == 'undefined') + { + debug("reachability does not exist. Setting to true"); + loginData.reachability = true; + } + // force it - this may not be the problem + loginData.reachability = true; + + // and now, force enable it + loginData.useNphZms = true; + loginData.useNphZmsForEvents = true; + + if (typeof loginData.packMontage == 'undefined') + { + debug("packMontage does not exist. Setting to false"); + loginData.packMontage = false; + } + + if (typeof loginData.forceNetworkStop == 'undefined') + { + debug("forceNetwork does not exist. Setting to false"); + loginData.forceNetworkStop = false; + } + + if (typeof loginData.enableLogs == 'undefined') + { + debug("enableLogs does not exist. Setting to true"); + loginData.enableLogs = true; + } + + if (typeof loginData.defaultPushSound == 'undefined') + { + debug("defaultPushSound does not exist. Setting to false"); + loginData.defaultPushSound = false; + } + + if (typeof loginData.exitOnSleep == 'undefined') + { + debug("exitOnSleep does not exist. Setting to false"); + loginData.exitOnSleep = false; + } + + if (typeof loginData.enableBlog == 'undefined') + { + debug("enableBlog does not exist. Setting to true"); + loginData.enableBlog = true; + + } + + if (typeof loginData.packeryPositionsArray == 'undefined') + { + debug("packeryPositionsArray does not exist. Setting to empty"); + loginData.packeryPositionsArray = {}; + + } + + + if (typeof loginData.packeryPositions == 'undefined') + { + debug("packeryPositions does not exist. Setting to empty"); + loginData.packeryPositions = ""; + + } + + if (typeof loginData.EHpackeryPositions == 'undefined') + { + debug("EHpackeryPositions does not exist. Setting to empty"); + loginData.EHpackeryPositions = ""; + + } + + if (typeof loginData.packerySizes == 'undefined') + { + debug("packerySizes does not exist. Setting to empty"); + loginData.packerySizes = ""; + + } + + if (typeof loginData.use24hr == 'undefined') + { + debug("use24hr does not exist. Setting to false"); + loginData.use24hr = false; + + } + + if (typeof timelineModalGraphType == 'undefined') + { + debug("timeline graph type not set. Setting to all"); + loginData.timelineModalGraphType = $translate.instant('kGraphAll'); + //console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" + loginData.timelineModalGraphType); + } + + if (typeof loginData.resumeDelay == 'undefined') + { + debug("resumeDelay does not exist. Setting to 0"); + loginData.resumeDelay = 0; + + } + // override resumeDelay - it was developed on a wrong assumption + loginData.resumeDelay = 0; + + if (typeof loginData.montageHistoryQuality == 'undefined') + { + debug("montageHistoryQuality does not exist. Setting to 50"); + loginData.montageHistoryQuality = "50"; + + } + + if (typeof loginData.disableNative == 'undefined') + { + debug("disableNative not found, setting to false"); + loginData.disableNative = false; + + } + + if (typeof loginData.vibrateOnPush == 'undefined') + { + debug("vibrate on push not found, setting to true"); + loginData.vibrateOnPush = true; + + } + + if (typeof loginData.soundOnPush == 'undefined') + { + debug("sound on push not found, setting to true"); + loginData.soundOnPush = true; + + } + + if (typeof loginData.cycleMonitors == 'undefined') + { + + loginData.cycleMonitors = false; + + } + + if (typeof loginData.cycleMonitorsInterval == 'undefined') + { + + loginData.cycleMonitorsInterval = 10; + + } + + if (typeof loginData.cycleMontage == 'undefined') + { + + loginData.cycleMontage = false; + + } + + if (typeof loginData.cycleMontageInterval == 'undefined') + { + + loginData.cycleMontageInterval = 10; + + } + + if (typeof loginData.enableLowBandwidth == 'undefined') + { + + loginData.enableLowBandwidth = false; + + } + // wtf is wrong with this ternary? + //$rootScope.runMode = (loginData.enableLowBandwith==true)? "low": "normal"; + + if (typeof loginData.autoSwitchBandwidth == 'undefined') + { + + loginData.autoSwitchBandwidth = false; + + } + + $rootScope.runMode = getBandwidth(); + log("Setting DataModel init bandwidth to: " + $rootScope.runMode); + + if (typeof loginData.refreshSecLowBW == 'undefined') + { + + loginData.refreshSecLowBW = 8; + + } + + if (typeof loginData.disableAlarmCheckMontage == 'undefined') + { + + loginData.disableAlarmCheckMontage = false; + + } + + if (typeof loginData.useLocalTimeZone == 'undefined') + { + + loginData.useLocalTimeZone = true; + + } + + if (typeof loginData.fastLogin == 'undefined') + { + + loginData.fastLogin = true; + + } + + if (typeof loginData.currentMontageProfile == 'undefined') + { + + loginData.currentMontageProfile = ''; + + } + + if (typeof loginData.followTimeLine == 'undefined') + { + + loginData.followTimeLine = false; + + } + + if (typeof loginData.timelineScale == 'undefined') + { + + loginData.timelineScale = -1; + + } + + + if (typeof loginData.showMontageSubMenu == 'undefined') + { + + loginData.showMontageSubMenu = false; + + } + + + + if (typeof loginData.monSingleImageQuality == 'undefined') + { + + loginData.monSingleImageQuality = 100; + + } + + if (typeof loginData.hideArchived == 'undefined') + { + + loginData.hideArchived = false; + + } + + if (typeof loginData.videoPlaybackSpeed == 'undefined') + { + + loginData.videoPlaybackSpeed = 2; + + } + + if (typeof loginData.enableGIFMP4 == 'undefined') + { + + loginData.enableGIFMP4 = true; + + } + + if (typeof loginData.enableSlowLoading == 'undefined') + { + + loginData.enableSlowLoading = false; + + } + log ("SlowDelay is: "+loginData.enableSlowLoading); + + if (typeof loginData.enableStrictSSL == 'undefined') + { + + loginData.enableStrictSSL = false; + + } + + log("DataModel init recovered this loginData as " + JSON.stringify(loginData)); + } + else + { + log("defaultServer configuration NOT found. Keeping login at defaults"); + } + + // now set up SSL - need to do it after data return + // from local forage + setSSLCerts(); + + + // FIXME: HACK: This is the latest entry point into dataModel init, so start portal login after this + // not the neatest way + $rootScope.$emit('init-complete'); + }); + + monitorsLoaded = 0; + //console.log("Getting out of NVRDataModel init"); + $rootScope.showBlog = loginData.enableBlog; + //debug("loginData structure values: " + JSON.stringify(loginData)); + + }); + + }, + + isForceNetworkStop: function() + { + return loginData.forceNetworkStop; + }, + + setJustResumed: function(val) + { + justResumed = true; + }, + + stopNetwork: function(str) + { + var s = ""; + if (str) s = str + ":"; + if (justResumed) + { + // we don't call stop as we did stop on pause + log(s + " Not calling window stop as we just resumed"); + justResumed = false; + } + else + { + log(s + " Calling window.stop()"); + window.stop(); + } + }, + + isLoggedIn: function() + { + + if ((loginData.username != "" && loginData.password != "" && loginData.url != "" && + loginData.apiurl != "") || (loginData.isUseAuth != '1')) + { + return 1; + } + else + { + + return 0; + + } + }, + + getLanguages: function() + { + return languages; + }, + + setDefaultLanguage: function(l, permanent) + { + + if (!l) l = 'en'; + defaultLang = l; + var d = $q.defer(); + if (permanent) + { + //window.localStorage.setItem("defaultLang", l); + + //console.log("setting default lang"); + localforage.setItem("defaultLang", l) + .then(function(val) + { + log("Set language in localforage to: " + val); + }); + } + + //console.log("invoking translate use with " + l); + $translate.use(l).then(function(data) + { + log("Device Language is:" + data); + moment.locale(data); + $translate.fallbackLanguage('en'); + d.resolve(data); + return d.promise; + }, function(error) + { + log("Device Language error: " + error); + $translate.use('en'); + moment.locale('en'); + d.resolve('en'); + return d.promise; + }); + return d.promise; + }, + + getDefaultLanguage: function() + { + return defaultLang; + //return window.localStorage.getItem("defaultLang"); + + }, + + reloadMonitorDisplayStatus: function() + { + return reloadMonitorDisplayStatus(); + }, + + getLogin: function() + { + + return angular.copy(loginData); + }, + + getServerGroups: function() + { + return angular.copy(serverGroupList); + }, + + setServerGroups: function(sg) + { + serverGroupList = angular.copy(sg); + }, + + getKeepAwake: function() + { + return (loginData.keepAwake == '1') ? true : false; + }, + + setAppVersion: function(ver) + { + zmAppVersion = ver; + }, + + getAppVersion: function() + { + return (zmAppVersion); + }, + + setBackground: function(val) + { + isBackground = val; + }, + + isBackground: function() + { + return isBackground; + }, + + isFirstUse: function() + { + // console.log("isFirstUse is " + isFirstUse); + return isFirstUse; + // return ((window.localStorage.getItem("isFirstUse") == undefined) ? true : false); + + }, + + versionCompare: function(l, r) + { + return versionCompare(l, r); + }, + + //----------------------------------------------------------------- + // Allow the option to reset first use if I need it in future + //----------------------------------------------------------------- + setFirstUse: function(val) + { + //window.localStorage.setItem("isFirstUse", val ? "1" : "0"); + //localforage.setItem("isFirstUse", val, + // function(err) {if (err) log ("localforage error, //storing isFirstUse: " + JSON.stringify(err));}); + isFirstUse = val; + localforage.setItem("isFirstUse", val); + //console.log (">>>>>>setting isFirstUse to " + val); + + }, + + getTimeFormat: function() + { + return (loginData.use24hr ? "HH:mm" : "hh:mm a"); + }, + + getTimeFormatSec: function() + { + return (loginData.use24hr ? "HH:mm:ss" : "hh:mm:ss a"); + }, + + //------------------------------------------------------------------ + // switches screen to 'always on' or 'auto' + //------------------------------------------------------------------ + setAwake: function(val) + { + + //console.log ("**** setAwake called with:" + val); + // log("Switching screen always on to " + val); + if (val) + { + + if (window.cordova != undefined) + { + window.plugins.insomnia.keepAwake(); + } + else + { + //console.log ("Skipping insomnia, cordova does not exist"); + } + } + else + { + if (window.cordova != undefined) + { + window.plugins.insomnia.allowSleepAgain(); + } + else + { + //console.log ("Skipping insomnia, cordova does not exist"); + } + + } + + }, + + //-------------------------------------------------------------------------- + // writes all params to local storage. FIXME: Move all of this into a JSON + // object + //-------------------------------------------------------------------------- + setLogin: function(newLogin) + { + + setLogin(newLogin); + $rootScope.showBlog = newLogin.enableBlog; + + }, + + //------------------------------------------------------- + // returns API version or none + //------------------------------------------------------- + getAPIversion: function() + { + debug("getAPIversion called"); + var d = $q.defer(); + var apiurl = loginData.apiurl + '/host/getVersion.json'; + $http.get(apiurl) + .then(function(success) + { + if (success.data.version) + { + $rootScope.apiValid = true; + d.resolve(success.data.version); + } + else + { + $rootScope.apiValid = false; + d.reject("-1.-1.-1"); + } + return (d.promise); + + }, + function(error) + { + debug("getAPIversion error handler " + JSON.stringify(error)); + d.reject("-1.-1.-1"); + $rootScope.apiValid = false; + return (d.promise); + }); + return (d.promise); + + }, + + displayBanner: function(mytype, mytext, myinterval, mytimer) + { + displayBanner(mytype, mytext, myinterval, mytimer); + }, + + isReCaptcha: function() + { + var d = $q.defer(); + + var myurl = loginData.url; + log("Checking if reCaptcha is enabled in ZM..."); + $http.get(myurl) + .then(function(success) + { + if (success.data.search("g-recaptcha") != -1) + { + // recaptcha enable. zmNinja won't work + log("ZM has recaptcha enabled", "error"); + displayBanner('error', ['Recaptcha must be disabled in Zoneminder', $rootScope.appName + ' will not work with recaptcha'], "", 8000); + d.resolve(true); + return (d.promise); + + } + else + { + d.resolve(false); + log("ZM has recaptcha disabled - good"); + return (d.promise); + } + }); + return (d.promise); + }, + + //----------------------------------------------------------------------------- + // Grabs the computed auth key for streaming + // FIXME: Currently a hack - does a screen parse - convert to API based support + //----------------------------------------------------------------------------- + + // need a mid as restricted users won't be able to get + // auth with just &watch + getAuthKey: function(mid, ck) + { + var d = $q.defer(); + + if (!mid) + { + log("Deferring auth key, as monitorId unknown"); + d.resolve(""); + return (d.promise); + } + + // Skipping monitor number as I only need an auth key + // so no need to generate an image + var myurl = loginData.url + "/index.php?view=watch&mid=" + mid + "&connkey=" + ck; + debug("DataModel: Getting auth from " + myurl + " with mid=" + mid); + $http.get(myurl) + .then(function(success) + { + // console.log ("**** RESULT IS " + JSON.stringify(success)); + // Look for auth= + var auth = success.data.match("auth=(.*?)&"); + if (auth && (auth[1] != null)) + { + log("DataModel: Extracted a stream authentication key of: " + auth[1]); + d.resolve("&auth=" + auth[1]); + } + else + { + log("DataModel: Did not find a stream auth key, looking for user="); + auth = success.data.match("user=(.*?)&"); + if (auth && (auth[1] != null)) + { + log("DataModel: Found simple stream auth mode (user=)"); + d.resolve("&user=" + loginData.username + "&pass=" + loginData.password); + } + else + { + log("Data Model: Did not find any stream mode of auth"); + d.resolve(""); + } + return (d.promise); + } + + }, + function(error) + { + log("DataModel: Error resolving auth key " + JSON.stringify(error)); + d.resolve(""); + return (d.promise); + }); + return (d.promise); + + }, + + //----------------------------------------------------------------------------- + // This function returns the numdigits for padding capture images + //----------------------------------------------------------------------------- + + getKeyConfigParams: function(forceReload) + { + + var d = $q.defer(); + + if (forceReload == 1 || configParams.ZM_EVENT_IMAGE_DIGITS == '-1') + { + var apiurl = loginData.apiurl; + var myurl = apiurl + '/configs/viewByName/ZM_EVENT_IMAGE_DIGITS.json'; + debug("Config URL for digits is:" + myurl); + $http.get(myurl) + .success(function(data) + { + log("ZM_EVENT_IMAGE_DIGITS is " + data.config.Value); + configParams.ZM_EVENT_IMAGE_DIGITS = data.config.Value; + d.resolve(configParams.ZM_EVENT_IMAGE_DIGITS); + return (d.promise); + + }) + .error(function(err) + { + log("Error retrieving ZM_EVENT_IMAGE_DIGITS" + JSON.stringify(err), "error"); + log("Taking a guess, setting ZM_EVENT_IMAGE_DIGITS to 5"); + // FIXME: take a plunge and keep it at 5? + configParams.ZM_EVENT_IMAGE_DIGITS = 5; + d.resolve(configParams.ZM_EVENT_IMAGE_DIGITS); + return (d.promise); + }); + } + else + { + log("ZM_EVENT_IMAGE_DIGITS is already configured for " + + configParams.ZM_EVENT_IMAGE_DIGITS); + d.resolve(configParams.ZM_EVENT_IMAGE_DIGITS); + } + return (d.promise); + + }, + + //-------------------------------------------------------------------------- + // Useful to know what ZMS is using as its cgi-bin. If people misconfigure + // the setting in the app, they can check their logs + //-------------------------------------------------------------------------- + getPathZms: function() + { + var d = $q.defer(); + var apiurl = loginData.apiurl; + var myurl = apiurl + '/configs/viewByName/ZM_PATH_ZMS.json'; + debug("Config URL for ZMS PATH is:" + myurl); + $http.get(myurl) + .success(function(data) + { + configParams.ZM_PATH_ZMS = data.config.Value; + d.resolve(configParams.ZM_PATH_ZMS); + return (d.promise); + }) + .error(function(error) + { + log("Can't retrieving ZM_PATH_ZMS: " + JSON.stringify(error)); + d.resolve(""); + return (d.promise); + }); + return (d.promise); + + }, + //-------------------------------------------------------------------------- + // returns high or low BW mode + //-------------------------------------------------------------------------- + getBandwidth: function() + { + return getBandwidth(); + }, + + //----------------------------------------------------------------------------- + // This function returns a list of monitors + // if forceReload == 1 then it will force an HTTP API request to get a list of monitors + // if 0. then it will return back the previously loaded monitor list if one exists, else + // will issue a new HTTP API to get it + + // I've wrapped this function in my own promise even though http returns a promise. + //----------------------------------------------------------------------------- + // + + // returns a non promise version + // so if monitors is null, it will return null + // As of now, this is only used by EventServer.js to + // send the right list of monitors after registration + // token + getMonitorsNow: function() + { + return monitors; + }, + + getMonitors: function(forceReload) + { + //console.log("** Inside ZMData getMonitors with forceReload=" + forceReload); + + $ionicLoading.show( + { + template: $translate.instant('kLoadingMonitors'), + animation: 'fade-in', + showBackdrop: true, + duration: zm.loadingTimeout, + maxWidth: 200, + showDelay: 0 + }); + + var d = $q.defer(); + if ((monitorsLoaded == 0) || (forceReload == 1)) // monitors are empty or force reload + { + //console.log("NVRDataModel: Invoking HTTP get to load monitors"); + log((forceReload == 1) ? "getMonitors:Force reloading all monitors" : "getMonitors:Loading all monitors"); + var apiurl = loginData.apiurl; + var myurl = apiurl + "/monitors.json"; + //console.log ("API:"+myurl); + $http.get(myurl /*,{timeout:15000}*/ ) + .success(function(data) + { + //console.log("HTTP success got " + JSON.stringify(data.monitors)); + monitors = data.monitors; + monitors.sort(function(a, b) + { + return parseInt(a.Monitor.Sequence) - parseInt(b.Monitor.Sequence); + }); + //console.log("promise resolved inside HTTP success"); + monitorsLoaded = 1; + + reloadMonitorDisplayStatus(); + + debug("Now trying to get multi-server data, if present"); + $http.get(apiurl + "/servers.json") + .success(function(data) + { + // We found a server list API, so lets make sure + // we get the hostname as it will be needed for playback + log("multi server list loaded" + JSON.stringify(data)); + multiservers = data.servers; + + for (var i = 0; i < monitors.length; i++) + { + + // make them all show for now + monitors[i].Monitor.listDisplay = 'show'; + monitors[i].Monitor.isAlarmed = false; + monitors[i].Monitor.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + + var serverFound = false; + for (var j = 0; j < multiservers.length; j++) + { + //console.log ("Comparing " + multiservers[j].Server.Id + " AND " + monitors[i].Monitor.ServerId); + if (multiservers[j].Server.Id == monitors[i].Monitor.ServerId) + { + //console.log ("Found match"); + serverFound = true; + break; + } + + } + if (serverFound) + { + + debug("Monitor " + monitors[i].Monitor.Id + " has a recording server hostname of " + multiservers[j].Server.Hostname); + + // Now here is the logic, I need to retrieve serverhostname, + // and slap on the host protocol and path. Meh. + + var p = URI.parse(loginData.streamingurl); + var s = URI.parse(multiservers[j].Server.Hostname); + + debug("recording server parsed is " + JSON.stringify(s)); + debug("portal parsed is " + JSON.stringify(p)); + + var st = ""; + var baseurl = ""; + + st += (s.scheme ? s.scheme : p.scheme) + "://"; // server scheme overrides + + // if server doesn't have a protocol, what we want is in path + if (!s.host) + { + s.host = s.path; + s.path = undefined; + } + + st += s.host; + + if (p.port || s.port) + { + st += (s.port ? ":" + s.port : ":" + p.port); + + } + + baseurl = st; + + st += (s.path ? s.path : p.path); + + //console.log ("----------STREAMING URL PARSED AS " + st); + + monitors[i].Monitor.streamingURL = st; + monitors[i].Monitor.baseURL = baseurl; + // starting 1.30 we have fid=xxx mode to return images + monitors[i].Monitor.imageMode = (versionCompare($rootScope.apiVersion, "1.30") == -1) ? "path" : "fid"; + debug("API " + $rootScope.apiVersion + ": Monitor " + monitors[i].Monitor.Id + " will use " + monitors[i].Monitor.imageMode + " for direct image access"); + + //debug ("Streaming URL for Monitor " + monitors[i].Monitor.Id + " is " + monitors[i].Monitor.streamingURL ); + //debug ("Base URL for Monitor " + monitors[i].Monitor.Id + " is " + monitors[i].Monitor.baseURL ); + + } + else + { + //monitors[i].Monitor.listDisplay = 'show'; + monitors[i].Monitor.isAlarmed = false; + monitors[i].Monitor.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + monitors[i].Monitor.streamingURL = loginData.streamingurl; + monitors[i].Monitor.baseURL = loginData.url; + monitors[i].Monitor.imageMode = (versionCompare($rootScope.apiVersion, "1.30") == -1) ? "path" : "fid"; + + // but now check if forced path + if (loginData.forceImageModePath) + { + debug("Overriding, setting image mode to true as you have requested force enable"); + monitors[i].Monitor.imageMode = 'path'; + } + + debug("API " + $rootScope.apiVersion + ": Monitor " + monitors[i].Monitor.Id + " will use " + monitors[i].Monitor.imageMode + " for direct image access"); + } + } + // now get packery hide if applicable + reloadMonitorDisplayStatus(); + d.resolve(monitors); + }) + .error(function(err) + { + log("multi server list loading error"); + multiservers = []; + + for (var i = 0; i < monitors.length; i++) + { + //monitors[i].Monitor.listDisplay = 'show'; + monitors[i].Monitor.isAlarmed = false; + monitors[i].Monitor.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + monitors[i].Monitor.streamingURL = loginData.streamingurl; + monitors[i].Monitor.baseURL = loginData.url; + monitors[i].Monitor.imageMode = (versionCompare($rootScope.apiVersion, "1.30") == -1) ? "path" : "fid"; + debug("API " + $rootScope.apiVersion + ": Monitor " + monitors[i].Monitor.Id + " will use " + monitors[i].Monitor.imageMode + " for direct image access"); + + } + d.resolve(monitors); + + }); + + $ionicLoading.hide(); + log("Monitor load was successful, loaded " + monitors.length + " monitors"); + + }) + .error(function(err) + { + //console.log("HTTP Error " + err); + log("Monitor load failed " + JSON.stringify(err), "error"); + // To keep it simple for now, I'm translating an error + // to imply no monitors could be loaded. FIXME: conver to proper error + monitors = []; + //console.log("promise resolved inside HTTP fail"); + displayBanner('error', ['error retrieving monitor list', 'please try again']); + d.resolve(monitors); + $ionicLoading.hide(); + monitorsLoaded = 0; + }); + return d.promise; + + } + else // monitors are loaded + { + //console.log("Returning pre-loaded list of " + monitors.length + " monitors"); + log("Returning pre-loaded list of " + monitors.length + " monitors"); + d.resolve(monitors); + //console.log ("Returning"+JSON.stringify(monitors)); + $ionicLoading.hide(); + return d.promise; + } + + }, + + //----------------------------------------------------------------------------- + // + //----------------------------------------------------------------------------- + setMonitors: function(mon) + { + //console.log("ZMData setMonitors called with " + mon.length + " monitors"); + monitors = mon; + }, + + processFastLogin: function() + { + var d = $q.defer(); + if (1) + { + d.reject("not implemented"); + return d.promise; + } + console.log("inside processFastLogin"); + if (!loginData.fastLogin) + { + console.log("Fast login not set"); + d.reject("fast login not enabled"); + debug("fast login not enabled"); + return d.promise; + + } + else //fastlogin is on + { + localforage.getItem("lastLogin") + .then(function(succ) + { + console.log("fast login DB found"); + var dt = moment(succ); + + if (dt.isValid()) + { + debug("Got last login as " + dt.toString()); + if (moment.duration(moment().diff(dt)).asHours() >= 2) + { + d.reject("duration since last login >=2hrs, need to relogin"); + return d.promise; + } + else + { + d.resolve("fast login is valid, less then 2 hrs"); + return d.promise; + } + } + else + { + console.log("Invalid date found"); + d.reject("last-login invalid"); + return d.promise; + + } + }, + function(e) + { + console.log("fastlogin DB not found"); + d.reject("last-login not found, fastlogin rejected"); + return d.promise; + }); + + } + return d.promise; + }, + + // returns if this mid is hidden or not + isNotHidden: function(mid) + { + var notHidden = true; + for (var i = 0; i < monitors.length; i++) + { + if (monitors[i].Monitor.Id == mid) + { + notHidden = (monitors[i].Monitor.listDisplay == 'show') ? true : false; + break; + } + + } + return notHidden; + + }, + + getLocalTimeZoneNow: function() + { + return moment.tz.guess(); + }, + //returns TZ value immediately (sync) + + getTimeZoneNow: function() + { + // console.log ("getTimeZoneNow: " + tz ? tz : moment.tz.guess()); + return tz ? tz : moment.tz.guess(); + }, + + // returns server timezone, failing which local timezone + // always resolves true + + isTzSupported: function() + { + return isTzSupported; + }, + + getTimeZone: function(isForce) + { + + var d = $q.defer(); + if (!tz || isForce) + { + + log("First invocation of TimeZone, asking server"); + var apiurl = loginData.apiurl + '/host/getTimeZone.json'; + $http.get(apiurl) + .then(function(success) + { + tz = success.data.tz; + d.resolve(tz); + debug("Timezone API response is:" + success.data.tz); + if (success.data.tz !== undefined) + isTzSupported = true; + else + isTzSupported = false; + $rootScope.$emit('tz-updated'); + return (d.promise); + + }, + function(error) + { + tz = moment.tz.guess(); + debug("Timezone API error handler, guessing local:" + tz); + d.resolve(tz); + isTzSupported = false; + return (d.promise); + }); + + } + else + { + d.resolve(tz); + return d.promise; + } + + return d.promise; + }, + + //----------------------------------------------------------------------------- + // When I display events in the event controller, this is the first function I call + // This returns the total number of pages + // I then proceed to display pages in reverse order to display the latest events first + // I also reverse sort them in NVRDataModel to sort by date + // All this effort because the ZM APIs return events in sorted order, oldest first. Yeesh. + //----------------------------------------------------------------------------- + + getEventsPages: function(monitorId, startTime, endTime) + { + //console.log("********** INSIDE EVENTS PAGES "); + var apiurl = loginData.apiurl; + + var myurl = apiurl + "/events/index"; + if (monitorId != 0) + myurl = myurl + "/MonitorId:" + monitorId; + if (startTime) + myurl = myurl + "/StartTime >=:" + startTime; + if (endTime) + myurl = myurl + "/EndTime <=:" + endTime; + + myurl = myurl + "/AlarmFrames >=:" + (loginData.enableAlarmCount ? loginData.minAlarmCount : 0); + + myurl = myurl + ".json"; + //console.log (">>>>>Constructed URL " + myurl); + + $ionicLoading.show( + { + template: $translate.instant('kCalcEventSize') + '...', + animation: 'fade-in', + showBackdrop: true, + duration: zm.loadingTimeout, + maxWidth: 200, + showDelay: 0 + }); + + //var myurl = (monitorId == 0) ? apiurl + "/events.json?page=1" : apiurl + "/events/index/MonitorId:" + monitorId + ".json?page=1"; + var d = $q.defer(); + $http.get(myurl) + .success(function(data) + { + $ionicLoading.hide(); + //console.log ("**** EVENTS PAGES I GOT "+JSON.stringify(data)); + //console.log("**** PAGE COUNT IS " + data.pagination.pageCount); + d.resolve(data.pagination); + return d.promise; + }) + .error(function(error) + { + $ionicLoading.hide(); + // console.log("*** ERROR GETTING TOTAL PAGES ***"); + log("Error retrieving page count of events " + JSON.stringify(error), "error"); + displayBanner('error', ['error retrieving event page count', 'please try again']); + + d.reject(error); + return d.promise; + }); + return d.promise; + + }, + + //----------------------------------------------------------------------------- + // This function returns events for specific monitor or all monitors + // You get here by tapping on events in the monitor screen or from + // the menu events option + // monitorId == 0 means all monitors (ZM starts from 1) + //----------------------------------------------------------------------------- + + getEvents: function(monitorId, pageId, loadingStr, startTime, endTime) + { + + //console.log("ZMData getEvents called with ID=" + monitorId + "and Page=" + pageId); + + if (!loadingStr) + { + loadingStr = $translate.instant('kLoadingEvents')+"..."; + } + //if (loadingStr) loa + + if (loadingStr != 'none') + { + $ionicLoading.show( + { + template: loadingStr, + animation: 'fade-in', + showBackdrop: true, + maxWidth: 200, + showDelay: 0, + duration: zm.loadingTimeout, //specifically for Android - http seems to get stuck at times + }); + } + + var d = $q.defer(); + var myevents = []; + var apiurl = loginData.apiurl; + + var myurl = apiurl + "/events/index"; + if (monitorId != 0) + myurl = myurl + "/MonitorId:" + monitorId; + if (startTime) + myurl = myurl + "/StartTime >=:" + startTime; + if (endTime) + myurl = myurl + "/EndTime <=:" + endTime; + + myurl = myurl + "/AlarmFrames >=:" + (loginData.enableAlarmCount ? loginData.minAlarmCount : 0); + myurl = myurl + ".json"; + + if (pageId) + { + myurl = myurl + "?page=" + pageId; + } + else + { + //console.log("**** PAGE WAS " + pageId); + } + + // Simulated data + + // myurl = "https://api.myjson.com/bins/4jx44.json"; + + //console.log (">>>>>Constructed URL " + myurl); + + $http.get(myurl /*,{timeout:15000}*/ ) + .success(function(data) + { + if (loadingStr != 'none') $ionicLoading.hide(); + //myevents = data.events; + myevents = data.events.reverse(); + if (monitorId == 0) + { + oldevents = myevents; + } + //console.log (JSON.stringify(data)); + // console.log("DataModel Returning " + myevents.length + "events for page" + pageId); + d.resolve(myevents); + return d.promise; + + }) + .error(function(err) + { + if (loadingStr != 'none') $ionicLoading.hide(); + displayBanner('error', ['error retrieving event list', 'please try again']); + //console.log("HTTP Events error " + err); + log("Error fetching events for page " + pageId + " Err: " + JSON.stringify(err), "error"); + // I need to reject this as I have infinite scrolling + // implemented in EventCtrl.js --> and if it does not know + // it got an error going to the next page, it will get into + // an infinite loop as we are at the bottom of the list always + + d.reject(myevents); + + // FIXME: Check what pagination does to this logic + if (monitorId == 0) + { + oldevents = []; + } + return d.promise; + }); + return d.promise; + }, + + //----------------------------------------------------------------------------- + // + //----------------------------------------------------------------------------- + getMontageSize: function() + { + return loginData.montageSize; + }, + + //----------------------------------------------------------------------------- + // + //----------------------------------------------------------------------------- + setMontageSize: function(montage) + { + loginData.montageSize = montage; + }, + + //----------------------------------------------------------------------------- + // + //----------------------------------------------------------------------------- + getMonitorsLoaded: function() + { + // console.log("**** Inside promise function "); + var deferred = $q.defer(); + if (monitorsLoaded != 0) + { + deferred.resolve(monitorsLoaded); + } + + return deferred.promise; + }, + + //----------------------------------------------------------------------------- + // + //----------------------------------------------------------------------------- + setMonitorsLoaded: function(loaded) + { + // console.log("ZMData.setMonitorsLoaded=" + loaded); + monitorsLoaded = loaded; + }, + + //----------------------------------------------------------------------------- + // returns the next monitor ID in the list + // used for swipe next + //----------------------------------------------------------------------------- + getNextMonitor: function(monitorId, direction) + { + var id = parseInt(monitorId); + var foundIndex = -1; + for (var i = 0; i < monitors.length; i++) + { + if (parseInt(monitors[i].Monitor.Id) == id) + { + foundIndex = i; + break; + } + } + if (foundIndex != -1) + { + foundIndex = foundIndex + direction; + // wrap around if needed + if (foundIndex < 0) foundIndex = monitors.length - 1; + if (foundIndex >= monitors.length) foundIndex = 0; + return (monitors[foundIndex].Monitor.Id); + } + else + { + log("getNextMonitor could not find monitor " + monitorId); + return (monitorId); + } + + }, + + //----------------------------------------------------------------------------- + // Given a monitor Id it returns the monitor name + // FIXME: Can I do a better job with associative arrays? + //----------------------------------------------------------------------------- + getMonitorName: function(id) + { + var idnum = parseInt(id); + for (var i = 0; i < monitors.length; i++) + { + if (parseInt(monitors[i].Monitor.Id) == idnum) + { + // console.log ("Matched, exiting getMonitorname"); + return monitors[i].Monitor.Name; + } + + } + return "(Unknown)"; + }, + + getMonitorObject: function(id) + { + var idnum = parseInt(id); + for (var i = 0; i < monitors.length; i++) + { + if (parseInt(monitors[i].Monitor.Id) == idnum) + { + // console.log ("Matched, exiting getMonitorname"); + return monitors[i]; + } + + } + return "(Unknown)"; + }, + + getImageMode: function(id) + { + var idnum = parseInt(id); + for (var i = 0; i < monitors.length; i++) + { + if (parseInt(monitors[i].Monitor.Id) == idnum) + { + // console.log ("Matched, exiting getMonitorname"); + return monitors[i].Monitor.imageMode; + } + + } + return "(Unknown)"; + }, + + getStreamingURL: function(id) + { + var idnum = parseInt(id); + for (var i = 0; i < monitors.length; i++) + { + if (parseInt(monitors[i].Monitor.Id) == idnum) + { + // console.log ("Matched, exiting getMonitorname"); + return monitors[i].Monitor.streamingURL; + } + + } + return "(Unknown)"; + }, + + getBaseURL: function(id) + { + var idnum = parseInt(id); + for (var i = 0; i < monitors.length; i++) + { + if (parseInt(monitors[i].Monitor.Id) == idnum) + { + // console.log ("Matched, exiting getMonitorname"); + return monitors[i].Monitor.baseURL; + } + + } + return "(Unknown)"; + }, + + }; + } +]); diff --git a/www/js/DevOptionsCtrl.js b/www/js/DevOptionsCtrl.js new file mode 100644 index 00000000..970c690d --- /dev/null +++ b/www/js/DevOptionsCtrl.js @@ -0,0 +1,139 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console */ + +angular.module('zmApp.controllers').controller('zmApp.DevOptionsCtrl', ['$scope', '$rootScope', '$ionicModal', 'zm', 'NVRDataModel', '$ionicSideMenuDelegate', '$ionicPopup', '$http', '$q', '$ionicLoading', '$ionicHistory', '$state', 'SecuredPopups', '$translate', function($scope, $rootScope, $ionicModal, zm, NVRDataModel, $ionicSideMenuDelegate, $ionicPopup, $http, $q, $ionicLoading, $ionicHistory, $state, SecuredPopups, $translate) +{ + + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + // $scope.this.will.crash = 1; + + }; + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + //---------------------------------------------------------------- + // Save anyway when you exit + //---------------------------------------------------------------- + + $scope.$on('$ionicView.beforeLeave', function() + { + saveDevOptions(); + + }); + + //------------------------------------------------------------------------- + // Lets make sure we set screen dim properly as we enter + // The problem is we enter other states before we leave previous states + // from a callback perspective in ionic, so we really can't predictably + // reset power state on exit as if it is called after we enter another + // state, that effectively overwrites current view power management needs + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + //console.log("**VIEW ** DevOptions Ctrl Entered"); + $scope.loginData = NVRDataModel.getLogin(); + + NVRDataModel.setAwake(false); + }); + + $scope.isTzSupported = function() + { + return NVRDataModel.isTzSupported(); + }; + + $scope.getTimeZoneNow = function() + { + return NVRDataModel.getTimeZoneNow(); + }; + + //------------------------------------------------------------------ + // Perform the login action when the user submits the login form + //------------------------------------------------------------------ + + function saveDevOptions() + { + NVRDataModel.debug("SaveDevOptions: called"); + + if (parseInt($scope.loginData.cycleMonitorsInterval) < zm.minCycleTime) + { + $scope.loginData.cycleMonitorsInterval = zm.minCycleTime.toString(); + } + if ((parseInt($scope.loginData.maxFPS) < 0) || (parseInt($scope.loginData.maxFPS) > zm.maxFPS)) + { + $scope.loginData.maxFPS = zm.defaultFPS.toString(); + } + + if (parseInt($scope.loginData.refreshSec) <= 0) + { + NVRDataModel.debug("SaveDevOptions: refresh sec was too low at " + + $scope.loginData.refreshSec + " reset to 1"); + $scope.loginData.refreshSec = 1; + + } + + if ((parseInt($scope.loginData.montageQuality) < zm.safeMontageLimit) || + (parseInt($scope.loginData.montageQuality) > 100)) + { + $scope.loginData.montageQuality = 100; + } + + if ((parseInt($scope.loginData.singleImageQuality) < zm.safeImageQuality) || + (parseInt($scope.loginData.singleImageQuality) > 100)) + { + $scope.loginData.singleImageQuality = zm.safeImageQuality.toString(); + } + + NVRDataModel.debug("SaveDevOptions: Saving to disk"); + NVRDataModel.setLogin($scope.loginData); + NVRDataModel.getMonitors(1); + + } + + $scope.saveDevOptions = function() + { + + saveDevOptions(); + // $rootScope.zmPopup.close(); + $rootScope.zmPopup = SecuredPopups.show('alert', + { + title: $translate.instant('kSettingsSaved'), + template: "{{'kExploreEnjoy' | translate }} {{$root.appName}}", + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }).then(function(res) + { + $ionicSideMenuDelegate.toggleLeft(); + }); + + }; + //------------------------------------------------------------------ + // controller main + //------------------------------------------------------------------ + +}]); diff --git a/www/js/EventCtrl.js b/www/js/EventCtrl.js new file mode 100644 index 00000000..260ecf47 --- /dev/null +++ b/www/js/EventCtrl.js @@ -0,0 +1,3002 @@ +/* jshint -W041 */ +/*jshint bitwise: false*/ +/* jslint browser: true*/ +/* global saveAs, cordova,StatusBar,angular,console,moment, MobileAccessibility, gifshot, ReadableStream , LibraryHelper, GifWriter, NeuQuant, LocalFileSystem, FileError*/ + +// This is the controller for Event view. StateParams is if I recall the monitor ID. +// This was before I got access to the new APIs. FIXME: Revisit this code to see what I am doing with it +// and whether the new API has a better mechanism + +angular.module('zmApp.controllers') + +// alarm frames filter +.filter('selectFrames', function($filter, $translate) +{ + + // Create the return function and set the required parameter name to **input** + return function(input, typeOfFrames) + { + + var out = []; + + angular.forEach(input, function(item) + { + + if (typeOfFrames == $translate.instant('kShowTimeDiffFrames')) + { + if (item.type == $translate.instant('kShowTimeDiffFrames')) + out.push(item); + } + else + out.push(item); + + }); + + return out; + }; + +}) + +.controller('zmApp.EventCtrl', ['$scope', '$rootScope', 'zm', 'NVRDataModel', 'message', '$ionicSideMenuDelegate', '$timeout', '$interval', '$ionicModal', '$ionicLoading', '$http', '$state', '$stateParams', '$ionicHistory', '$ionicScrollDelegate', '$ionicPlatform', '$ionicSlideBoxDelegate', '$ionicPosition', '$ionicPopover', '$ionicPopup', 'EventServer', '$sce', '$cordovaBadge', '$cordovaLocalNotification', '$q', 'carouselUtils', '$translate', '$cordovaFileTransfer', '$cordovaFile', '$ionicListDelegate',function($scope, $rootScope, zm, NVRDataModel, message, $ionicSideMenuDelegate, $timeout, $interval, $ionicModal, $ionicLoading, $http, $state, $stateParams, $ionicHistory, $ionicScrollDelegate, $ionicPlatform, $ionicSlideBoxDelegate, $ionicPosition, $ionicPopover, $ionicPopup, EventServer, $sce, $cordovaBadge, $cordovaLocalNotification, $q, carouselUtils, $translate, $cordovaFileTransfer, $cordovaFile, $ionicListDelegate) +{ + + // events in last 5 minutes + // TODO https://server/zm/api/events/consoleEvents/5%20minute.json + + //--------------------------------------------------- + // Controller main + //--------------------------------------------------- + + var loginData; + var oldEvent; + var scrollbynumber; + var eventImageDigits = 5; // failsafe + var eventsPage; + var moreEvents; + var pageLoaded; + var enableLoadMore; + var lData; + var showHiddenMonitors; + var ionRangeWatcher; + var mycarouselWatcher; + var nolangFrom; + var nolangTo; + + $scope.typeOfFrames = $translate.instant('kShowTimeDiffFrames'); + $scope.outlineMotion = false; + $scope.outlineMotionParam = ""; + var eventsListScrubHeight = eventsListScrubHeight; + var eventsListDetailsHeight = eventsListDetailsHeight; + + //--------------------------------------------------- + // initial code + //--------------------------------------------------- + + //we come here is TZ is updated after the view loads + $rootScope.$on('tz-updated', function() + { + $scope.tzAbbr = NVRDataModel.getTimeZoneNow(); + NVRDataModel.debug("Timezone API updated timezone to " + NVRDataModel.getTimeZoneNow()); + }); + + $rootScope.$on("language-changed", function() + { + NVRDataModel.log(">>>>>>>>>>>>>>> language changed"); + doRefresh(); + }); + + $scope.$on('$ionicView.afterEnter', function() + { + //console.log ("********* AFTER ENTER"); + // + $ionicListDelegate.canSwipeItems(true); + NVRDataModel.debug ("enabling options swipe"); + + // see if we come from monitors, if so, don't filter events + if ($ionicHistory.backTitle() == 'Monitors') + { + showHiddenMonitors = true; + } + else + { + showHiddenMonitors = false; + } + + if (NVRDataModel.getLogin().useLocalTimeZone) + { + $scope.tzAbbr = moment().tz(moment.tz.guess()).zoneAbbr(); + } + else + { + $scope.tzAbbr = moment().tz(NVRDataModel.getTimeZoneNow()).zoneAbbr(); + } + + $scope.events = []; + getInitialEvents(); + setupWatchers(); + footerExpand(); + }); + + $scope.$on('$ionicView.beforeEnter', function() + { + + //console.log ("********* BEFORE ENTER"); + // + $scope.gifshotSupported = true; + document.addEventListener("pause", onPause, false); + //console.log("I got STATE PARAM " + $stateParams.id); + $scope.id = parseInt($stateParams.id, 10); + $scope.showEvent = $stateParams.playEvent || false; + + console.log(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + + NVRDataModel.log("EventCtrl called with: EID=" + $scope.id + " playEvent = " + $scope.showEvent); + + // This is the only view that hardcodes row size due to + // collection repeat, so lets re-get the text size if it has changed + // note that there may be a delay as its a callback - so might involve + // a UI jiggle + + if (window.cordova) + MobileAccessibility.getTextZoom(getTextZoomCallback); + + eventsListDetailsHeight = parseInt(zm.eventsListDetailsHeight * $rootScope.textScaleFactor); + eventsListScrubHeight = parseInt(zm.eventsListScrubHeight * $rootScope.textScaleFactor); + + NVRDataModel.debug(">>>height of list/scrub set to " + eventsListDetailsHeight + " and " + eventsListScrubHeight); + + pageLoaded = false; + enableLoadMore = true; + + $scope.mycarousel = { + index: 0 + }; + + $scope.ionRange = { + index: 1 + }; + $scope.animationInProgress = false; + + $scope.hours = []; + $scope.days = []; + $scope.weeks = []; + $scope.months = []; + + $scope.eventList = { + showDelete: false + }; + + $scope.slides = []; // will hold scrub frames + $scope.totalEventTime = 0; // used to display max of progress bar + $scope.currentEventTime = 0; + oldEvent = ""; // will hold previous event that had showScrub = true + scrollbynumber = 0; + $scope.eventsBeingLoaded = true; + $scope.FrameArray = []; // will hold frame info from detailed Events API + loginData = NVRDataModel.getLogin(); + NVRDataModel.getKeyConfigParams(0) + .then(function(data) + { + //console.log ("***GETKEY: " + JSON.stringify(data)); + eventImageDigits = parseInt(data); + NVRDataModel.log("Image padding digits reported as " + eventImageDigits); + }); + + $scope.showSearch = false; + eventsPage = 1; + moreEvents = true; + $scope.viewTitle = { + title: "" + }; + $scope.search = { + text: "" + + }; + $scope.myfilter = ""; + + $scope.loginData = NVRDataModel.getLogin(); + $scope.playbackURL = $scope.loginData.url; + + }); + + function getEventObject(eid) + { + + var apiurl = NVRDataModel.getLogin().apiurl + '/events/' + eid + '.json'; + + $http.get(apiurl) + .success(function(data) {}) + .error(function(err) {}); + + } + + function getTextZoomCallback(tz) + { + $rootScope.textScaleFactor = parseFloat(tz + "%") / 100.0; + NVRDataModel.debug("text zoom factor is " + $rootScope.textScaleFactor); + } + + // -------------------------------------------------------- + // Handling of back button in case modal is open should + // close the modal + // -------------------------------------------------------- + + $ionicPlatform.registerBackButtonAction(function(e) + { + e.preventDefault(); + if ($scope.modal != undefined && $scope.modal.isShown()) + { + // switch off awake, as liveview is finished + NVRDataModel.debug("Modal is open, closing it"); + NVRDataModel.setAwake(false); + $scope.modal.remove(); + } + else + { + NVRDataModel.debug("Modal is closed, so toggling or exiting"); + if (!$ionicSideMenuDelegate.isOpenLeft()) + { + $ionicSideMenuDelegate.toggleLeft(); + + } + else + { + navigator.app.exitApp(); + } + + } + + }, 1000); + + //-------------------------------------- + // monitor the slider for carousels + //-------------------------------------- + function setupWatchers() + { + NVRDataModel.debug("Setting up carousel watchers"); + + ionRangeWatcher = $scope.$watch('ionRange.index', function() + { + // console.log ("Watching index"); + $scope.mycarousel.index = parseInt($scope.ionRange.index) - 1; + if (carouselUtils.getStop() == true) + return; + + //console.log ("***ION RANGE CHANGED TO " + $scope.mycarousel.index); + }); + + mycarouselWatcher = $scope.$watch('mycarousel.index', function() + { + + if ($scope.event && $scope.ionRange.index == parseInt($scope.event.Event.Frames) - 1) + { + if (!$scope.modal || $scope.modal.isShown() == false) + { + // console.log("quick scrub playback over"); + carouselUtils.setStop(true); + $scope.ionRange.index = 0; + $scope.mycarousel.index = 1; + } + + } + if (carouselUtils.getStop() == true) + return; + $scope.ionRange.index = ($scope.mycarousel.index + 1).toString(); + // console.log ("***IONRANGE RANGE CHANGED TO " + $scope.ionRange.index); + + }); + + } + + // -------------------------------------------------------- + // Handling of back button in case modal is open should + // close the modal + // -------------------------------------------------------- + + function getInitialEvents() + { + NVRDataModel.debug("getInitialEvents called"); + var lData = NVRDataModel.getLogin(); + + // If you came from Monitors, disregard hidden monitors in montage + /* if (lData.persistMontageOrder && stackState != "Monitors") { + var tempMon = message; + $scope.monitors = NVRDataModel.applyMontageMonitorPrefs(tempMon, 2)[0]; + } else*/ + $scope.monitors = message; + + if ($scope.monitors.length == 0) + { + var pTitle = $translate.instant('kNoMonitors'); + $ionicPopup.alert( + { + title: pTitle, + template: "{{'kCheckCredentials' | translate }}", + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("login", + { + "wizard": false + }); + return; + } + + $scope.events = []; + + // First get total pages and then + // start from the latest. If this fails, nothing displays + + NVRDataModel.debug("EventCtrl: grabbing # of event pages"); + nolangFrom = ""; + nolangTo = ""; + if ($rootScope.fromString) + nolangFrom = moment($rootScope.fromString).locale('en').format("YYYY-MM-DD HH:mm:ss"); + if ($rootScope.toString) + nolangTo = moment($rootScope.toString).locale('en').format("YYYY-MM-DD HH:mm:ss"); + NVRDataModel.getEventsPages($scope.id, nolangFrom, nolangTo) + .then(function(data) + { + eventsPage = data.pageCount || 1; + NVRDataModel.debug("EventCtrl: found " + eventsPage + " pages of events"); + + pageLoaded = true; + $scope.viewTitle.title = data.count; + NVRDataModel.debug("EventCtrl: grabbing events for: id=" + $scope.id + " Date/Time:" + $rootScope.fromString + + "-" + $rootScope.toString); + nolangFrom = ""; + nolangTo = ""; + if ($rootScope.fromString) + nolangFrom = moment($rootScope.fromString).locale('en').format("YYYY-MM-DD HH:mm:ss"); + if ($rootScope.toString) + nolangTo = moment($rootScope.toString).locale('en').format("YYYY-MM-DD HH:mm:ss"); + + NVRDataModel.getEvents($scope.id, eventsPage, "", nolangFrom, nolangTo) + .then(function(data) + { + + var myevents = data; + NVRDataModel.debug("EventCtrl: success, got " + myevents.length + " events"); + var loginData = NVRDataModel.getLogin(); + for (var i = 0; i < myevents.length; i++) + { + + var idfound = true; + if (loginData.persistMontageOrder) + { + idfound = false; + for (var ii = 0; ii < $scope.monitors.length; ii++) + { + if ($scope.monitors[ii].Monitor.Id == myevents[i].Event.MonitorId && (NVRDataModel.isNotHidden(myevents[i].Event.MonitorId) || showHiddenMonitors)) + { + + idfound = true; + break; + } + } + } + + myevents[i].Event.humanizeTime = humanizeTime(myevents[i].Event.StartTime); + myevents[i].Event.streamingURL = NVRDataModel.getStreamingURL(myevents[i].Event.MonitorId); + myevents[i].Event.baseURL = NVRDataModel.getBaseURL(myevents[i].Event.MonitorId); + myevents[i].Event.imageMode = NVRDataModel.getImageMode(myevents[i].Event.MonitorId); + + //console.log ("***** MULTISERVER STREAMING URL FOR EVENTS " + myevents[i].Event.streamingURL); + + // console.log ("***** MULTISERVER BASE URL FOR EVENTS " + myevents[i].Event.baseURL); + + myevents[i].Event.MonitorName = NVRDataModel.getMonitorName(myevents[i].Event.MonitorId); + myevents[i].Event.ShowScrub = false; + myevents[i].Event.height = eventsListDetailsHeight; + // now construct base path + myevents[i].Event.BasePath = computeBasePath(myevents[i]); + myevents[i].Event.relativePath = computeRelativePath(myevents[i]); + + // in multiserver BasePath is login url for frames + // http://login.url/index.php?view=frame&eid=19696772&fid=21 + + // console.log ("COMPARING "+NVRDataModel.getLogin().url+ " TO " +myevents[i].Event.baseURL); + if (NVRDataModel.getLogin().url != myevents[i].Event.baseURL) + { + //NVRDataModel.debug ("Multi server, changing base"); + myevents[i].Event.baseURL = NVRDataModel.getLogin().url; + + } + + if (myevents[i].Event.imageMode == 'path') + //if (1) + myevents[i].Event.videoPath = myevents[i].Event.baseURL + "/events/" + myevents[i].Event.relativePath + myevents[i].Event.DefaultVideo; + else + myevents[i].Event.videoPath = myevents[i].Event.baseURL + "/index.php?view=view_video&eid=" + myevents[i].Event.Id; + + if (idfound) + { + $scope.events.push(myevents[i]); + } + else + { + //console.log ("Skipping Event MID = " + myevents[i].Event.MonitorId); + } + + } //for + + //$scope.events = myevents; + // we only need to stop the template from loading when the list is empty + // so this can be false once we have _some_ content + // FIXME: check reload + $scope.eventsBeingLoaded = false; + // to avoid only few events being displayed + // if last page has less events + //console.log("**Loading Next Page ***"); + if (myevents.length < 50) + { + NVRDataModel.debug("EventCtrl:loading one more page just in case we don't have enough to display"); + loadMore(); + } + }); + + }); + } + + //------------------------------------------------------- + // Tapping on a frame shows this image + //------------------------------------------------------ + + function SaveSuccess() + { + $ionicLoading.show( + { + template: $translate.instant('kDone'), + noBackdrop: true, + duration: 1000 + }); + NVRDataModel.debug("ModalCtrl:Photo saved successfuly"); + } + + function SaveError(e) + { + $ionicLoading.show( + { + template: $translate.instant('kErrorSave'), + noBackdrop: true, + duration: 2000 + }); + NVRDataModel.log("Error saving image: " + e.message); + //console.log("***ERROR"); + } + + function saveNow(imgsrc, r, f) + { + + $ionicLoading.show( + { + template: $translate.instant('kSavingSnapshot') + "...", + noBackdrop: true, + duration: zm.httpTimeout + }); + var url = imgsrc; + NVRDataModel.log("saveNow: File path to grab is " + url); + + var img = new Image(); + img.onload = function() + { + // console.log("********* ONLOAD"); + var canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + var context = canvas.getContext('2d'); + context.drawImage(img, 0, 0); + + var imageDataUrl = canvas.toDataURL('image/jpeg', 1.0); + var imageData = imageDataUrl.replace(/data:image\/jpeg;base64,/, ''); + + if ($rootScope.platformOS != "desktop") + { + try + { + + cordova.exec( + SaveSuccess, + SaveError, + 'Canvas2ImagePlugin', + 'saveImageDataToLibrary', [imageData] + ); + // carouselUtils.setStop(curState); + } + catch (e) + { + + SaveError(e.message); + // carouselUtils.setStop(curState); + } + } + else + { + + var fname = r + f + ".png"; + fname = fname.replace(/\//, "-"); + fname = fname.replace(/\.jpg/, ''); + + canvas.toBlob(function(blob) + { + saveAs(blob, fname); + SaveSuccess(); + }); + } + }; + try + { + img.src = url; + // console.log ("SAVING IMAGE SOURCE"); + } + catch (e) + { + SaveError(e.message); + } + + } + + + + + function writeFile2( path, file, blob, isAppend) + { + var csize = 4 * 1024 * 1024; // 4MB + var d = $q.defer(); + NVRDataModel.debug ("Inside writeFile2 with blob size="+blob.size); + + // nothing more to write, so all good? + if (!blob.size) + { + NVRDataModel.debug ("writeFile2 all done"); + d.resolve(true); + return $q.resolve(true); + } + + + if (!isAppend) + { + // return the delegated promise, even if it fails + return $cordovaFile.writeFile(path, file, blob.slice(0,csize), true) + .then (function (succ) { + return writeFile2(path,file,blob.slice(csize),true); + }); + } + else + { + // return the delegated promise, even if it fails + return $cordovaFile.writeExistingFile(path, file, blob.slice(0,csize)) + .then (function (succ) { + return writeFile2(path,file,blob.slice(csize),true); + }); + } + + + } + + function writeFile(path, __filename, __data){ + var d = $q.defer(); + console.log ("inside write file"); + window.requestFileSystem(LocalFileSystem.TEMPORARY, __data.size+5000, onFileSystemSuccess, fail); + + function fail(e) + { + var msg = ''; + + switch (e.code) { + case FileError.QUOTA_EXCEEDED_ERR: + msg = 'QUOTA_EXCEEDED_ERR'; + break; + case FileError.NOT_FOUND_ERR: + msg = 'NOT_FOUND_ERR'; + break; + case FileError.SECURITY_ERR: + msg = 'SECURITY_ERR'; + break; + case FileError.INVALID_MODIFICATION_ERR: + msg = 'INVALID_MODIFICATION_ERR'; + break; + case FileError.INVALID_STATE_ERR: + msg = 'INVALID_STATE_ERR'; + break; + default: + msg = 'Unknown Error'; + break; + } + + console.log('Error: ' + msg); + } + function onFileSystemSuccess() + { + console.log ("Got temporary FS"); + window.resolveLocalFileSystemURL(path, function(dir){ + dir.getFile(__filename, {create:true}, function(file){ + file.createWriter(function(fileWriter){ + //var blob = new Blob([__data], {type:'text/plain'}); + console.log ("about to write "+__data.size+" bytes"); + //var blob = new Blob([__data], {type:'text/plain'}); + fileWriter.write(__data); + fileWriter.onwrite = function(e) { + NVRDataModel.debug ("write complete"); + d.resolve(); + return d.promise; + }; + + fileWriter.onerror = function(e) { + NVRDataModel.debug ("write error in filewriter:"+JSON.stringify(e)); + d.reject(); + return d.promise; + }; + + }); + }); + + }, + function (err) { + d.reject(err); + return d.promise; + }); + } + return d.promise; + } + + + function moveImageToGallery(fname) + { + // this is https://github.com/terikon/cordova-plugin-photo-library + + NVRDataModel.debug("moveImageToGallery called with " + fname); + cordova.plugins.photoLibrary.saveImage(fname, "zmNinja",onSuccess, onError); + //LibraryHelper.saveImageToLibrary(onSuccess, onError, fname, "zmNinja"); + + function onSuccess(results) + { + + NVRDataModel.debug("Removing temp file"); + + if ($rootScope.platformOS == 'ios') { + $cordovaFile.removeFile(cordova.file.documentsDirectory, "temp-file.gif"); + } + else + $cordovaFile.removeFile(cordova.file.dataDirectory, "temp-file.gif"); + $ionicLoading.show( + { + template: $translate.instant('kDone'), + noBackdrop: true, + duration: 2000 + }); + + + } + + function onError(error) + { + console.log("Error: " + error); + + } + } + + $scope.downloadFileToDevice = function(path, eid) + { + + NVRDataModel.setAwake(true); + var tp; + if ($rootScope.platformOS == 'ios') + tp = cordova.file.documentsDirectory + "temp-video.mp4"; + else + tp = cordova.file.dataDirectory + "temp-video.mp4"; + + var th = true; + var opt = {}; + //path = "http://techslides.com/demos/sample-videos/small.mp4"; + + NVRDataModel.debug("Saving temporary video to: " + tp); + $cordovaFileTransfer.download(path, tp, opt, th) + .then(function(result) + { + NVRDataModel.debug("Moving to gallery..."); + var ntp; + ntp = tp.indexOf('file://') === 0 ? tp.slice(7) : tp; + + $timeout(function() + { + $ionicLoading.hide(); + }); + moveToGallery(ntp, eid + "-video"); + NVRDataModel.setAwake(false); + // Success! + }, function(err) + { + NVRDataModel.setAwake(false); + NVRDataModel.log("Error=" + JSON.stringify(err)); + + $timeout(function() + { + $ionicLoading.show( + { + + template: $translate.instant('kError'), + noBackdrop: true, + duration: 3000 + }); + }); + // Error + }, function(progress) + { + var p = Math.round((progress.loaded / progress.total) * 100); + + $ionicLoading.show( + { + + template: $translate.instant('kPleaseWait') + "...(" + p + "%)", + noBackdrop: true + }); + + }); + + function moveToGallery(path, fname) + { + + NVRDataModel.debug("moveToGallery called with " + path); + LibraryHelper.saveVideoToLibrary(onSuccess, onError, path, fname); + + function onSuccess(results) + { + NVRDataModel.debug("Removing temp file"); + + if ($rootScope.platformOS == 'ios') + $cordovaFile.removeFile(cordova.file.documentsDirectory, "temp-video.mp4"); + else + $cordovaFile.removeFile(cordova.file.dataDirectory, "temp-video.mp4"); + + } + + function onError(error) + { + console.log("Error: " + error); + + } + } + + }; + + $scope.mp4warning = function() + { + $ionicPopup.alert( + { + title: $translate.instant('kNote'), + template: "{{'kVideoMp4Warning' | translate }}", + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + }; + + $scope.showImage = function(p, r, f, fid, e, imode, id, parray, ndx) + { + var img; + + //console.log ("HERE"); + $scope.kFrame = $translate.instant('kFrame'); + $scope.kEvent = $translate.instant('kEvent'); + $scope.ndx = ndx; + $scope.parray = parray; + $scope.imode = imode; + + // note ndx may be incorrect if we are looking + // at unique frames; + + // NVRDataModel.debug("Hello"); + if ($scope.typeOfFrames == $translate.instant('kShowTimeDiffFrames')) + { + + var ic; + + for (ic = 0; ic < $scope.parray.length; ic++) + { + if ($scope.parray[ic].frameid == fid) + break; + } + + NVRDataModel.debug("Readjusting selected frame ID from:" + $scope.ndx + " to actual frame ID of:" + ic); + $scope.ndx = ic; + } + else + { + NVRDataModel.debug("No index adjustment necessary as we are using all frames"); + } + + // console.log ("Image Mode " + imode); + // console.log ("parray : " + JSON.stringify(parray)); + // console.log ("index: " + ndx); + if ($scope.imode == 'path') + { + if ($scope.outlineMotion) + $scope.imgsrc = p + "/index.php?view=image&path=" + r + $scope.parray[$scope.ndx].aname; + else + $scope.imgsrc = p + "/index.php?view=image&path=" + r + $scope.parray[$scope.ndx].fname; + $scope.fallbackImgSrc = p + "/index.php?view=image&path=" + r + $scope.parray[$scope.ndx].fname; + } + + else + { + $scope.imgsrc = p + "/index.php?view=image&fid=" + $scope.parray[$scope.ndx].id+$scope.outlineMotionParam; + $scope.fallbackImgSrc = p + "/index.php?view=image&fid=" + $scope.parray[$scope.ndx].id; + + } + + //$rootScope.zmPopup = $ionicPopup.alert({title: kFrame+':'+fid+'/'+kEvent+':'+e,template:img, cssClass:'popup80'}); + + $rootScope.zmPopup = $ionicPopup.show( + { + template: '<center>' + $translate.instant('kFrame') + ':{{parray[ndx].frameid}}@{{prettifyTimeSec(parray[ndx].time)}}</center><br/><img ng-src="{{imgsrc}}" fallback-src="{{fallbackImgSrc}}" width="100%" />', + title: $translate.instant('kImages') + " (" + $translate.instant($scope.typeOfFrames) + ")", + subTitle: 'use left and right arrows to change', + scope: $scope, + cssClass: 'popup95', + buttons: [ + + { + text: '', + type: 'button-assertive button-small ion-camera', + onTap: function(e) + { + e.preventDefault(); + saveNow($scope.imgsrc, r, parray[$scope.ndx].fname); + + } + }, + + { + // left 1 + text: '', + type: 'button-small button-energized ion-chevron-left', + onTap: function(e) + { + // look for next frame that matches the type of frame + // we are showing (all or diff timestamps); + + // console.log ("TYPE OF FRAMES: " + $scope.typeOfFrames); + var nndx = null; + var alltype = $translate.instant('kShowAllFrames'); + for (var i = $scope.ndx - 1; i >= 0; i--) + { + if ($scope.parray[i].type == $scope.typeOfFrames || $scope.typeOfFrames == alltype) + { + nndx = i; + break; + } + } + if (nndx == null) nndx = $scope.ndx; + $scope.ndx = nndx; + + if ($scope.imode == 'path') + { + if ($scope.outlineMotion) + $scope.imgsrc = p + "/index.php?view=image&path=" + r + $scope.parray[$scope.ndx].aname; + else + $scope.imgsrc = p + "/index.php?view=image&path=" + r + $scope.parray[$scope.ndx].fname; + $scope.fallbackImgSrc = p + "/index.php?view=image&path=" + r + $scope.parray[$scope.ndx].fname; + } + + else + { + $scope.imgsrc = p + "/index.php?view=image&fid=" + $scope.parray[$scope.ndx].id+$scope.outlineMotionParam; + $scope.fallbackImgSrc = p + "/index.php?view=image&fid=" + $scope.parray[$scope.ndx].id; + + } + + + e.preventDefault(); + + } + }, + { + // right 1 + text: '', + type: 'button-small button-energized ion-chevron-right', + onTap: function(e) + { + + // look for next frame that matches the type of frame + // we are showing (all or diff timestamps); + + // console.log ("TYPE OF FRAMES: " + $scope.typeOfFrames); + var nndx = null; + var alltype = $translate.instant('kShowAllFrames'); + for (var i = $scope.ndx + 1; i < $scope.parray.length; i++) + { + //console.log ("Comparing: " +$scope.parray[i].type +" to " + $scope.typeOfFrames); + if ($scope.parray[i].type == $scope.typeOfFrames || $scope.typeOfFrames == alltype) + { + nndx = i; + break; + } + } + if (nndx == null) nndx = $scope.ndx; + $scope.ndx = nndx; + + if ($scope.imode == 'path') + { + if ($scope.outlineMotion) + $scope.imgsrc = p + "/index.php?view=image&path=" + r + $scope.parray[$scope.ndx].aname; + else + $scope.imgsrc = p + "/index.php?view=image&path=" + r + $scope.parray[$scope.ndx].fname; + $scope.fallbackImgSrc = p + "/index.php?view=image&path=" + r + $scope.parray[$scope.ndx].fname; + } + + else + { + $scope.imgsrc = p + "/index.php?view=image&fid=" + $scope.parray[$scope.ndx].id+$scope.outlineMotionParam; + $scope.fallbackImgSrc = p + "/index.php?view=image&fid=" + $scope.parray[$scope.ndx].id; + + } + + + e.preventDefault(); + + } + }, + + { + text: '', + type: 'button-positive button-small ion-checkmark-round', + onTap: function(e) { + + } + } + ] + }); + + }; + + + + $scope.toggleMotionOutline = function() + { + $scope.outlineMotion = !$scope.outlineMotion; + if ($scope.outlineMotion) + $scope.outlineMotionParam = "&show=analyse"; + else + $scope.outlineMotionParam = ""; + }; + + $scope.toggleTypeOfAlarms = function() + { + // "kShowAllFrames" : "all", + // "kShowTimeDiffFrames" : "different timestamps" + + if ($scope.typeOfFrames == $translate.instant('kShowAllFrames')) + { + $scope.typeOfFrames = $translate.instant('kShowTimeDiffFrames'); + } + else + { + $scope.typeOfFrames = $translate.instant('kShowAllFrames'); + } + }; + + // not explictly handling error --> I have a default "No events found" message + // displayed in the template if events list is null + + //-------------------------------------------------------------------------- + // This is what the pullup bar calls depending on what range is specified + //-------------------------------------------------------------------------- + $scope.showEvents = function(val, unit, monitorId) + { + NVRDataModel.debug("ShowEvents called with val:" + val + " unit:" + unit + " for Monitor:" + monitorId); + + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + + // we have to convert from and to, to server time + var mToDate = moment().tz(NVRDataModel.getTimeZoneNow()); + var mFromDate = moment().subtract(parseInt(val), unit).tz(NVRDataModel.getTimeZoneNow()); + + // console.log("Moment Dates:" + mFromDate.format() + " TO " + mToDate.format()); + + $rootScope.fromTime = mFromDate.toDate(); + $rootScope.toTime = mToDate.toDate(); + $rootScope.fromDate = $rootScope.fromTime; + $rootScope.toDate = $rootScope.toTime; + + NVRDataModel.debug("From: " + $rootScope.fromTime); + NVRDataModel.debug("To: " + $rootScope.toTime); + + //$rootScope.fromDate = fromDate.toDate(); + //$rootScope.toDate = toDate.toDate(); + $rootScope.isEventFilterOn = true; + $rootScope.fromString = mFromDate + .format("YYYY-MM-DD") + " " + mFromDate.format("HH:mm:ss"); + + $rootScope.toString = mToDate + .format("YYYY-MM-DD") + " " + mToDate + .format("HH:mm:ss"); + + // console.log("**************From String: " + $rootScope.fromString); + // console.log("**************To String: " + $rootScope.toString); + + // reloading - may solve https://github.com/pliablepixels/zmNinja/issues/36 + // if you are in the same mid event page $state.go won't work + $state.go("events", + { + "id": monitorId, + "playEvent": false + }, + { + reload: true + }); + }; + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + // credit:http://stackoverflow.com/a/20151856/1361529 + function base64toBlob(base64Data, contentType) + { + contentType = contentType || ''; + var sliceSize = 1024; + var byteCharacters = atob(base64Data); + var bytesLength = byteCharacters.length; + var slicesCount = Math.ceil(bytesLength / sliceSize); + var byteArrays = new Array(slicesCount); + + for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) + { + var begin = sliceIndex * sliceSize; + var end = Math.min(begin + sliceSize, bytesLength); + + var bytes = new Array(end - begin); + for (var offset = begin, i = 0; offset < end; ++i, ++offset) + { + bytes[i] = byteCharacters[offset].charCodeAt(0); + } + byteArrays[sliceIndex] = new Uint8Array(bytes); + } + return new Blob(byteArrays, + { + type: contentType + }); + } + + //---------------------------------------------------------- + // create an array of images + // too keep memory manageable, we are only going to pick up alarmed frames + // and that too, max 1ps + // -------------------------------------------------------------- + function prepareImages(e) + { + var d = $q.defer(); + var imglist = []; + var myurl = loginData.apiurl + '/events/' + e.Event.Id + ".json"; + $http.get(myurl) + .then(function(succ) + { + var data = succ.data; + var fps = 0; + var lastTime = ""; + + for (var i = 0; i < data.event.Frame.length; i++) + { + if (data.event.Frame[i].Type == "Alarm") + //if (1) + { + var fname; + //console.log ("PATH="+e.Event.imageMode); + if (e.Event.imageMode == 'path') + //if (1) + { + var rfp = padToN(data.event.Frame[i].FrameId, eventImageDigits) + "-capture.jpg"; + fname = e.Event.baseURL + "/index.php?view=image&width=" + zm.maxGifWidth + "&path=" + e.Event.relativePath + rfp; + } + else + { + fname = e.Event.baseURL + "/index.php?view=image&width=" + zm.maxGifWidth + "&fid=" + data.event.Frame[i].Id; + } + + if (data.event.Frame[i].TimeStamp != lastTime /*|| fps < 2*/ ) + + { + imglist.push(fname); + //fps = data.event.Frame[i].TimeStamp != lastTime ? 0 : fps+1; + lastTime = data.event.Frame[i].TimeStamp; + } + + } + + } + + // next up make sure we are not processing more than 100 images + + while (imglist.length > zm.maxGifCount2) + { + NVRDataModel.debug("Too many images: " + imglist.length + ", deleting alternate frames to keep it <=" + zm.maxGifCount2); + + for (var l = 0; l < imglist.length; l++) + { + imglist.splice(l + 1, 2); + if (imglist.length <= zm.maxGifCount2) break; + } + + } + NVRDataModel.debug("final image list length is:" + imglist.length); + + d.resolve(imglist); + return d.promise; + }, + function(err) + { + d.reject(err); + return d.promise; + }); + return d.promise; + } + + // force image to be of zm.maxGifWidth. TBD: rotated foo + function adjustAspect(e) + { + + var w = zm.maxGifWidth; + var h = Math.round(e.Event.Height / e.Event.Width * zm.maxGifWidth); + return { + w: w, + h: h + }; + + } + + // for devices - handle permission before you download + $scope.permissionsDownload = function(e) + { + if ($rootScope.platformOS == 'desktop') + { + gifAlert(e); + } + else + { + + console.log("in perms"); + cordova.plugins.photoLibrary.getLibrary( + function(library) + { + gifAlert(e); + }, + function(err) + { + if (err.startsWith('Permission')) + { + // call requestAuthorization, and retry + cordova.plugins.photoLibrary.requestAuthorization( + function() + { + // User gave us permission to his library, retry reading it! + gifAlert(e); + }, + function(err) + { + NVRDataModel.log("ERROR with saving permissions " + err); + // User denied the access + }, // if options not provided, defaults to {read: true}. + { + read: true, + write: true + } + ); + } + // Handle error - it's not permission-related + } + ); + + } + }; + + // make sure the user knows the GIF is not full fps/all frames + function gifAlert(e) + { + if(navigator.userAgent.toLowerCase().indexOf('crosswalk') == -1) { + $ionicPopup.confirm( + { + title: $translate.instant('kNote'), + template: "{{'kGifWarning' | translate }}", + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }).then(function(res) + { + if (res) + { + downloadAsGif2(e); + } + else + NVRDataModel.debug ("User cancelled GIF"); + + }); + } + else + { + $ionicPopup.alert({ + title:$translate.instant ('kNote'), + template:"{{'kGifNoCrosswalk' | translate}}" + }); + } + + + } + + // convert to base64 - devices need this to save to gallery + function blobToBase64(blob) + { + NVRDataModel.debug("converting blob to base64..."); + var d = $q.defer(); + var reader = new window.FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = function() + { + var base64data = reader.result; + //console.log(base64data ); + d.resolve(base64data); + return d.promise; + + }; + return d.promise; + } + + // part of neuquant conversion + function componentizedPaletteToArray(paletteRGB) + { + var paletteArray = [], + i, r, g, b; + for (i = 0; i < paletteRGB.length; i += 3) + { + r = paletteRGB[i]; + g = paletteRGB[i + 1]; + b = paletteRGB[i + 2]; + paletteArray.push(r << 16 | g << 8 | b); + } + return paletteArray; + } + + // part of neuquant conversion + function dataToRGB(data, width, height) + { + var i = 0, + length = width * height * 4, + rgb = []; + while (i < length) + { + rgb.push(data[i++]); + rgb.push(data[i++]); + rgb.push(data[i++]); + i++; + } + return rgb; + } + + // credit Jimmy Warting + // https://github.com/jimmywarting/StreamSaver.js/issues/38 + // he stream-ized and cleaned up the gif creation process + // using GifWriter.js + function createGif(files, w, h) + { + + var cv = document.getElementById("canvas"); + var ctx = cv.getContext("2d"); + var pixels = new Uint8Array(w * h); + var totalImages = files.length; + var processedImages = 0; + + cv.width = w; + cv.height = h; + + var rs = new ReadableStream( + { + // Each time pull gets called you should get the pixel data and + // enqueue it as if it would be good old gif.addFrame() + pull: function pull(controller) + { + var frame = files.shift(); + if (!frame) {controller.close(); return;} + + return $http( + { + url: frame, + responseType: "blob" + }) + .then(function(res) + { + + return res.data.image(); + }) + .then(function(img) + { + processedImages++; + + var p = Math.round(processedImages / totalImages * 100); + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + "...(" + p + "%)", + noBackdrop: true + }); + + console.log("URL=" + frame); + URL.revokeObjectURL(img.src); + ctx.drawImage(img, 0, 0); + + var data = ctx.getImageData(0, 0, w, h).data; + var rgbComponents = dataToRGB(data, w, h); + var nq = new NeuQuant(rgbComponents, rgbComponents.length, 15); + var paletteRGB = nq.process(); + var paletteArray = new Uint32Array(componentizedPaletteToArray(paletteRGB)); + var numberPixels = w * h; + var k = 0, + i, r, g, b; + + for (i = 0; i < numberPixels; i++) + { + r = rgbComponents[k++]; + g = rgbComponents[k++]; + b = rgbComponents[k++]; + pixels[i] = nq.map(r, g, b); + } + + controller.enqueue([0, 0, w, h, pixels, + { + palette: paletteArray, + delay: 100, // 1 second + }]); + }); + } + }); + + return new GifWriter(rs, w, h, + { + loop: null + }); + } + + + + function downloadAsGif2(e) + { + $rootScope.isDownloading = true; + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + "...", + noBackdrop: true, + duration: 20000 + }); + NVRDataModel.setAwake(true); + + prepareImages(e) + .then(function(files) + { + return $http( + { + url: files[0], + responseType: "blob" + }) + .then(function(res) + { + return res.data.image(); + }) + .then(function(img) + { + URL.revokeObjectURL(img.src); // Revoke object URL to free memory + var stream = createGif(files, img.width, img.height); + //var fileStream = streamSaver.createWriteStream('image.gif'); + + var chunks = []; + var reader = stream.getReader(); + + function pull() + { + return reader.read().then(function(result) + { + chunks.push(result.value); + return result.done ? chunks : pull(); + }); + } + + pull().then(function(chunks) + { + var blob = new Blob(chunks, + { + type: "image/gif" + + }); + + //alert ("WE ARE DONE!"); + if ($rootScope.platformOS == 'desktop') + { + saveAs(blob, e.Event.Id + "-video.gif"); + $ionicLoading.hide(); + } + else + { + // write blob to file + var tp; + if ($rootScope.platformOS == 'ios') + tp = cordova.file.documentsDirectory; + else + tp = cordova.file.dataDirectory; + var th = true, opt = {}; + + $ionicLoading.show( + { + + template:"writing to file...", + noBackdrop: true, + }); + + //var bloburl = URL.createObjectURL(blob); + //NVRDataModel.debug ("blob-url is:"+bloburl); + + writeFile2(tp,"temp-file.gif",blob,false) + .then (function (succ) { + NVRDataModel.debug ("write to file successful"); + console.log( "write file successful"); + $ionicLoading.hide(); + + var ntp = tp; + //ntp = tp.indexOf('file://') === 0 ? tp.slice(7) : tp; + + ntp = ntp+"temp-file.gif"; + console.log ("ntp="+ntp); + + moveImageToGallery(ntp); + $rootScope.isDownloading = false; + + }, function (err) { + $rootScope.isDownloading = false; + $ionicLoading.hide(); + NVRDataModel.debug ("error writing to file "+JSON.stringify(err)); + + + }); + } + + }); + }); + + }, + function(err) + { + $ionicLoading.hide(); + NVRDataModel.setAwake(false); + NVRDataModel.log("Error getting frames"); + $rootScope.isDownloading = false; + } + + ); + + } + + // NOT USED - WILL REMOVE AFTER TESTING OTHER METHOD MORE + function downloadAsGif(e) + { + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + "...", + noBackdrop: true, + duration: 20000 + }); + + prepareImages(e) + .then(function(imgs) + { + + console.log("TOTAL IMAGES TO GIF=" + imgs.length); + //console.log(JSON.stringify(imgs)); + + var ad = adjustAspect(e); + //console.log("SAVING W=" + ad.w + " H=" + ad.h); + NVRDataModel.setAwake(true); + gifshot.createGIF( + { + + 'gifWidth': ad.w, + 'gifHeight': ad.h, + 'images': imgs, + 'interval': 1, + //'loop':null, + 'sampleInterval': 20, + //'frameDur':5, // 1/2 a sec + 'text': 'zmNinja', + 'crossOrigin': 'use-credentials', + 'progressCallback': function(cp) + { + var p = Math.round(cp * 100); + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + "...(" + p + "%)", + noBackdrop: true + }); + } + }, function(obj) + { + NVRDataModel.setAwake(false); + if (!obj.error) + { + //console.log(obj.image); + + var blob; + + if ($rootScope.platformOS == 'desktop') + { + + obj.image = obj.image.replace(/data:image\/gif;base64,/, ''); + blob = base64toBlob(obj.image, "image/gif"); + var f = NVRDataModel.getMonitorName(e.Event.MonitorId); + f = f + "-" + e.Event.Id + ".gif"; + saveAs(blob, f); + $ionicLoading.hide(); + } + + else + { + NVRDataModel.debug("Saving blob to gallery..."); + var album = "zmNinja"; + cordova.plugins.photoLibrary.saveImage(obj.image, album, + function() + { + $ionicLoading.hide(); + NVRDataModel.debug("Event saved"); + }, + function(err) + { + $ionicLoading.hide(); + NVRDataModel.debug("Saving ERROR=" + err); + }); + + } + + } + else + { + $ionicLoading.hide(); + NVRDataModel.log("Error creating GIF"); + } + }); + }, + function(err) + { + $ionicLoading.hide(); + NVRDataModel.log("Error getting frames"); + } + + ); + } + + $scope.archiveUnarchiveEvent = function (ndx,eid) + { + //https://server/zm/api/events/11902.json -XPUT -d"Event[Archived]=1" + // + $ionicListDelegate.closeOptionButtons(); + + NVRDataModel.debug ("Archiving request for EID="+eid); + var loginData = NVRDataModel.getLogin(); + var apiArchive = loginData.apiurl + "/events/" + eid + ".json"; + var setArchiveBit = ($scope.events[ndx].Event.Archived == '0') ? "1":"0"; + + NVRDataModel.debug ("Calling archive with:"+apiArchive+ " and Archive="+setArchiveBit); + //put(url, data, [config]); + + // $http.put(apiArchive,"Event[Archived]="+setArchiveBit) + // + $ionicLoading.show( + { + template: "{{'kPleaseWait' | translate}}...", + noBackdrop: true, + duration: zm.httpTimeout + }); + + $http({ + + method: 'POST', + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': '*/*', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + + encodeURIComponent(obj[p])); + var foo = str.join("&"); + // console.log("****RETURNING " + foo); + NVRDataModel.debug("MonitorCtrl: parmeters constructed: " + foo); + return foo; + }, + url: apiArchive, + data: { + "Event[Archived]":setArchiveBit + + } + }) + .then (function (success) { + + NVRDataModel.log ("archiving response: "+ JSON.stringify(success)); + if (success.data.message == 'Error') + { + $ionicLoading.show( + { + template: "{{'kError' | translate}}...", + noBackdrop: true, + duration: 1500 + }); + + } + else + { + + + $ionicLoading.show( + { + template: "{{'kSuccess' | translate}}...", + noBackdrop: true, + duration: 1000 + }); + if ($scope.events[ndx].Event.Archived == '0') + $scope.events[ndx].Event.Archived = '1'; + else + $scope.events[ndx].Event.Archived = '0'; + } + + + + }, + function (error) { + NVRDataModel.log ("Error archiving: "+ JSON.stringify(error)); + } ); + + + + }; + + //-------------------------------------------------------------------------- + // Takes care of deleting individual events + //-------------------------------------------------------------------------- + + $scope.deleteEvent = function(id, itemid) + { + //$scope.eventList.showDelete = false; + //curl -XDELETE http://server/zm/api/events/1.json + var loginData = NVRDataModel.getLogin(); + var apiDelete = loginData.apiurl + "/events/" + id + ".json"; + NVRDataModel.debug("DeleteEvent: ID=" + id + " item=" + itemid); + NVRDataModel.log("Delete event " + apiDelete); + + $ionicLoading.show( + { + template: "{{'kDeletingEvent' | translate}}...", + noBackdrop: true, + duration: zm.httpTimeout + }); + + + + $http.delete(apiDelete) + .success(function(data) + { + $ionicLoading.hide(); + NVRDataModel.debug("delete output: " + JSON.stringify(data)); + + if (data.message == 'Error') + { + $ionicLoading.show( + { + template: "{{'kError' | translate}}...", + noBackdrop: true, + duration: 1500 + }); + + } + else + { + + + $ionicLoading.show( + { + template: "{{'kSuccess' | translate}}...", + noBackdrop: true, + duration: 1000 + }); + $scope.events.splice(itemid, 1); + + } + + // NVRDataModel.displayBanner('info', [$translate.instant('kDeleteEventSuccess')], 2000, 2000); + + + + + //doRefresh(); + + }) + .error(function(data) + { + $ionicLoading.hide(); + NVRDataModel.debug("delete error: " + JSON.stringify(data)); + NVRDataModel.displayBanner('error', [$translate.instant('kDeleteEventError1'), $translate.instant('kDeleteEventError2')]); + }); + + }; + + //------------------------------------------------ + // Tapping on the filter sign lets you reset it + //------------------------------------------------- + + $scope.filterTapped = function() + { + //console.log("FILTER TAPPED"); + var myFrom = moment($rootScope.fromString).format("MMM/DD/YYYY " + NVRDataModel.getTimeFormat()).toString(); + var toString = moment($rootScope.toString).format("MMM/DD/YYYY " + NVRDataModel.getTimeFormat()).toString(); + + $rootScope.zmPopup = $ionicPopup.confirm( + { + title: $translate.instant('kFilterSettings'), + template: $translate.instant('kFilterEventsBetween1') + ':<br/> <b>' + myFrom + "</b> " + $translate.instant('kTo') + " <b>" + toString + '</b><br/>' + $translate.instant('kFilterEventsBetween2'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + $rootScope.zmPopup.then(function(res) + { + if (res) + { + NVRDataModel.log("Filter reset requested in popup"); + $rootScope.isEventFilterOn = false; + $rootScope.fromDate = ""; + $rootScope.fromTime = ""; + $rootScope.toDate = ""; + $rootScope.toTime = ""; + $rootScope.fromString = ""; + $rootScope.toString = ""; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }); + return; + } + else + { + NVRDataModel.log("Filter reset cancelled in popup"); + } + }); + + }; + + //-------------------------------------------------------------------------- + // When the user pulls the pullup bar we call this to get the latest + // data for events ranges summaries using the consolveEvents facility of ZM + //-------------------------------------------------------------------------- + + $scope.footerExpand = function() + { + footerExpand(); + + }; + + function footerExpand() + { + //https://server/zm/api/events/consoleEvents/5%20minute.json + var ld = NVRDataModel.getLogin(); + + var af = "/AlarmFrames >=:" + (ld.enableAlarmCount ? ld.minAlarmCount : 0); + + var apiurl = ld.apiurl + "/events/consoleEvents/1%20hour" + af + ".json"; + NVRDataModel.debug("consoleEvents API:" + apiurl); + + $http.get(apiurl) + .success(function(data) + { + NVRDataModel.debug(JSON.stringify(data)); + $scope.hours = []; + var p = data.results; + for (var key in data.results) + { + + if (p.hasOwnProperty(key)) + { + + var idfound = true; + //console.log ("PERSIST IS " + ld.persistMontageOrder); + if (ld.persistMontageOrder) + { + idfound = false; + for (var ii = 0; ii < $scope.monitors.length; ii++) + { + if ($scope.monitors[ii].Monitor.Id == key && (NVRDataModel.isNotHidden(key) || showHiddenMonitors)) + { + // console.log ("Authorizing "+$scope.monitors[ii].Monitor.Name); + idfound = true; + break; + } + } + } + //console.log(NVRDataModel.getMonitorName(key) + " -> " + p[key]); + if (idfound) + $scope.hours.push( + { + monitor: NVRDataModel.getMonitorName(key), + events: p[key], + mid: key + }); + + } + } + }); + + apiurl = ld.apiurl + "/events/consoleEvents/1%20day" + af + ".json"; + NVRDataModel.debug("consoleEvents API:" + apiurl); + $http.get(apiurl) + .success(function(data) + { + NVRDataModel.debug(JSON.stringify(data)); + $scope.days = []; + var p = data.results; + for (var key in data.results) + { + if (p.hasOwnProperty(key)) + { + var idfound = true; + if (ld.persistMontageOrder) + { + idfound = false; + for (var ii = 0; ii < $scope.monitors.length; ii++) + { + if ($scope.monitors[ii].Monitor.Id == key && (NVRDataModel.isNotHidden(key) || showHiddenMonitors)) + { + idfound = true; + break; + } + } + } + //console.log(NVRDataModel.getMonitorName(key) + " -> " + p[key]); + if (idfound) + //console.log(NVRDataModel.getMonitorName(key) + " -> " + p[key]); + $scope.days.push( + { + monitor: NVRDataModel.getMonitorName(key), + events: p[key], + mid: key + }); + + } + } + }); + + apiurl = ld.apiurl + "/events/consoleEvents/1%20week" + af + ".json"; + NVRDataModel.debug("consoleEvents API:" + apiurl); + $http.get(apiurl) + .success(function(data) + { + NVRDataModel.debug(JSON.stringify(data)); + $scope.weeks = []; + var p = data.results; + for (var key in data.results) + { + if (p.hasOwnProperty(key)) + { + + var idfound = true; + if (ld.persistMontageOrder) + { + idfound = false; + for (var ii = 0; ii < $scope.monitors.length; ii++) + { + if ($scope.monitors[ii].Monitor.Id == key && (NVRDataModel.isNotHidden(key) || showHiddenMonitors)) + { + idfound = true; + break; + } + } + } + //console.log(NVRDataModel.getMonitorName(key) + " -> " + p[key]); + if (idfound) + //console.log(NVRDataModel.getMonitorName(key) + " -> " + p[key]); + $scope.weeks.push( + { + monitor: NVRDataModel.getMonitorName(key), + events: p[key], + mid: key + }); + + } + } + }); + + apiurl = ld.apiurl + "/events/consoleEvents/1%20month" + af + ".json"; + NVRDataModel.debug("consoleEvents API:" + apiurl); + $http.get(apiurl) + .success(function(data) + { + NVRDataModel.debug(JSON.stringify(data)); + $scope.months = []; + var p = data.results; + for (var key in data.results) + { + if (p.hasOwnProperty(key)) + { + + var idfound = true; + var ld = NVRDataModel.getLogin(); + if (ld.persistMontageOrder) + { + idfound = false; + for (var ii = 0; ii < $scope.monitors.length; ii++) + { + if ($scope.monitors[ii].Monitor.Id == key && (NVRDataModel.isNotHidden(key) || showHiddenMonitors)) + { + idfound = true; + break; + } + } + } + //console.log(NVRDataModel.getMonitorName(key) + " -> " + p[key]); + if (idfound) + //console.log(NVRDataModel.getMonitorName(key) + " -> " + p[key]); + $scope.months.push( + { + monitor: NVRDataModel.getMonitorName(key), + events: p[key], + mid: key + }); + + } + } + }); + + } + + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + $scope.scrollPosition = function() + { + var scrl = parseFloat($ionicScrollDelegate.$getByHandle("mainScroll").getScrollPosition().top); + var item = Math.round(scrl / eventsListDetailsHeight); + if ($scope.events == undefined || !$scope.events.length || $scope.events[item] == undefined) + { + return ""; + } + else + { + //return prettifyDate($scope.events[item].Event.StartTime); + return ($scope.events[item].Event.humanizeTime); + } + //return Math.random(); + }; + + //------------------------------------------------------------------------- + // called when user switches to background + //------------------------------------------------------------------------- + function onPause() + { + NVRDataModel.debug("EventCtrl:onpause called"); + if ($scope.popover) $scope.popover.remove(); + + } + //------------------------------------------------------------------------- + // Pads the filename with leading 0s, depending on ZM_IMAGE_DIGITS + //------------------------------------------------------------------------- + function padToN(number, digits) + { + + var i; + var stringMax = ""; + var stringLeading = ""; + for (i = 1; i <= digits; i++) + { + stringMax = stringMax + "9"; + if (i != digits) stringLeading = stringLeading + "0"; + } + var numMax = parseInt(stringMax); + + if (number <= numMax) + { + number = (stringLeading + number).slice(-digits); + } + //console.log ("PADTON: returning " + number); + return number; + } + + //------------------------------------------------------------------------- + // FIXME: Are we using this? + //------------------------------------------------------------------------- + $scope.disableSlide = function() + { + NVRDataModel.debug("EventCtrl:DisableSlide called"); + $ionicSlideBoxDelegate.$getByHandle("eventSlideBox").enableSlide(false); + }; + + $scope.checkSwipe = function (ndx) + { + if ($scope.events[ndx].Event.ShowScrub) + { + $ionicListDelegate.canSwipeItems(false); + NVRDataModel.debug ("disabling options swipe"); + } + else + { + $ionicListDelegate.canSwipeItems(true); + NVRDataModel.debug ("enabling options swipe"); + } + + }; + + //------------------------------------------------------------------------- + // This function is called when a user enables or disables + // scrub view for an event. + //------------------------------------------------------------------------- + + $scope.toggleGroupScrub = function(event, ndx, frames) + { + $scope.groupType = "scrub"; + toggleGroup(event, ndx, frames, $scope.groupType); + }; + + $scope.toggleGroupAlarms = function(event, ndx, frames) + { + $scope.groupType = "alarms"; + toggleGroup(event, ndx, frames, $scope.groupType); + }; + + function toggleGroup(event, ndx, frames, groupType) + { + + + // If we are here and there is a record of a previous scroll + // then we need to scroll back to hide that view + if (scrollbynumber) + { + $ionicScrollDelegate.$getByHandle("mainScroll").scrollBy(0, -1 * scrollbynumber, true); + scrollbynumber = 0; + } + + if (oldEvent && event != oldEvent) + { + + NVRDataModel.debug("EventCtrl:Old event scrub will hide now"); + oldEvent.Event.ShowScrub = false; + oldEvent.Event.height = eventsListDetailsHeight; + oldEvent = ""; + } + + event.Event.ShowScrub = !event.Event.ShowScrub; + + if (event.Event.ShowScrub == false) + { + $ionicListDelegate.canSwipeItems(true); + NVRDataModel.debug ("enabling options swipe due to toggle"); + } + + else + { + $ionicListDelegate.canSwipeItems(false); + $ionicListDelegate.closeOptionButtons(); + NVRDataModel.debug ("disabling options swipe due to toggle"); + + } + + + + + //console.log ("SCRUBBING IS "+event.Event.ShowScrub); + // $ionicScrollDelegate.resize(); + + //console.log ("GROUP TYPE IS " + groupType); + + if (event.Event.ShowScrub == true) // turn on display now + { + + + if (groupType == 'alarms') + { + // $ionicListDelegate.canSwipeItems(false); + //NVRDataModel.debug ("Disabling flag swipe as alarms are swipable"); + $scope.alarm_images = []; + event.Event.height = (eventsListDetailsHeight + eventsListScrubHeight); + $ionicScrollDelegate.resize(); + var myurl = loginData.apiurl + '/events/' + event.Event.Id + ".json"; + NVRDataModel.log("API for event details" + myurl); + $http.get(myurl) + .success(function(data) + { + $scope.FrameArray = data.event.Frame; + // $scope.slider_options.scale=[]; + + //$scope.slider_options.scale = []; + + var i; + var timestamp = null; + for (i = 0; i < data.event.Frame.length; i++) + { + if (data.event.Frame[i].Type == "Alarm") + { + + //console.log ("**ONLY ALARM AT " + i + "of " + data.event.Frame.length); + var atype; + if (timestamp != data.event.Frame[i].TimeStamp) + { + + atype = $translate.instant('kShowTimeDiffFrames'); + } + else + { + atype = $translate.instant('kShowAllFrames'); + } + $scope.alarm_images.push( + { + type: atype, + id: data.event.Frame[i].Id, + frameid: data.event.Frame[i].FrameId, + score: data.event.Frame[i].Score, + fname: padToN(data.event.Frame[i].FrameId, eventImageDigits) + "-capture.jpg", + aname:padToN(data.event.Frame[i].FrameId, eventImageDigits) + "-analyse.jpg", + time: data.event.Frame[i].TimeStamp + }); + timestamp = data.event.Frame[i].TimeStamp; + } + + } + oldEvent = event; + + //console.log (JSON.stringify(data)); + }) + .error(function(err) + { + NVRDataModel.log("Error retrieving detailed frame API " + JSON.stringify(err)); + NVRDataModel.displayBanner('error', ['could not retrieve frame details', 'please try again']); + }); + + } // end of groupType == alarms + else // groupType == scrub + { + + NVRDataModel.debug("EventCtrl: Scrubbing will turn on now"); + $scope.currentEvent = ""; + $scope.event = event; + //$ionicScrollDelegate.freezeScroll(true); + $ionicSideMenuDelegate.canDragContent(false); + $scope.slider_options = { + from: 1, + to: event.Event.Frames, + realtime: true, + step: 1, + className: "mySliderClass", + callback: function(value, released) + { + //console.log("CALLBACK"+value+released); + $ionicScrollDelegate.freezeScroll(!released); + //NVRDataModel.debug("EventCtrl: freezeScroll called with " + !released); + + }, + //modelLabels:function(val) {return "";}, + css: + { + background: + { + "background-color": "silver" + }, + before: + { + "background-color": "purple" + }, + default: + { + "background-color": "white" + }, // default value: 1px + after: + { + "background-color": "green" + }, // zone after default value + pointer: + { + "background-color": "red" + }, // circle pointer + range: + { + "background-color": "red" + } // use it if double value + }, + scale: [] + + }; + + event.Event.height = (eventsListDetailsHeight + eventsListScrubHeight); + $ionicScrollDelegate.resize(); + $scope.mycarousel.index = 0; + $scope.ionRange.index = 1; + //console.log("**Resetting range"); + $scope.slides = []; + var i; + + if (event.Event.imageMode == 'path') + { + NVRDataModel.debug("EventCtrl: found " + frames + " frames to scrub"); + + for (i = 1; i <= frames; i++) + { + var fname = padToN(i, eventImageDigits) + "-capture.jpg"; + + $scope.slides.push( + { + id: i, + img: fname + }); + + } + } + else // we need fids + { + var myurl_frames = loginData.apiurl + '/events/' + event.Event.Id + ".json"; + NVRDataModel.log("API for event details" + myurl_frames); + $http.get(myurl_frames) + .success(function(data) + { + $scope.FrameArray = data.event.Frame; + // $scope.slider_options.scale=[]; + + //$scope.slider_options.scale = []; + + var i; + for (i = 0; i < data.event.Frame.length; i++) + { + + //console.log ("**ONLY ALARM AT " + i + "of " + data.event.Frame.length); + $scope.slides.push( + { + id: data.event.Frame[i].Id, + frameid: data.event.Frame[i].FrameId, + + }); + + } + + //console.log (JSON.stringify(data)); + }) + .error(function(err) + { + NVRDataModel.log("Error retrieving detailed frame API " + JSON.stringify(err)); + NVRDataModel.displayBanner('error', [$translate.instant('kErrorFrameBanner'), $translate.instant('kErrorPleaseTryAgain')]); + }); + + } + + // now get event details to show alarm frames + loginData = NVRDataModel.getLogin(); + + if (typeof event.Event.DefaultVideo === 'undefined') + event.Event.DefaultVideo = ""; + // grab video details + event.Event.video = {}; + var videoURL; + + //if (event.Event.imageMode == 'path') + if (1) + videoURL = event.Event.baseURL + "/events/" + event.Event.relativePath + event.Event.DefaultVideo; + else + videoURL = event.Event.baseURL + "/index.php?view=view_video&eid=" + event.Event.Id; + + console.log("************** VIDEO IS " + videoURL); + event.Event.video.config = { + autoPlay: true, + sources: [ + { + src: $sce.trustAsResourceUrl(videoURL), + type: "video/mp4" + } + + ], + + theme: "lib/videogular-themes-default/videogular.css", + + }; + + var myurl2 = loginData.apiurl + '/events/' + event.Event.Id + ".json"; + NVRDataModel.log("API for event details" + myurl2); + $http.get(myurl2) + .success(function(data) + { + $scope.FrameArray = data.event.Frame; + // $scope.slider_options.scale=[]; + $scope.slider_options.scale = []; + + var i; + for (i = 0; i < data.event.Frame.length; i++) + { + if (data.event.Frame[i].Type == "Alarm") + { + + //console.log ("**ALARM AT " + i + "of " + data.event.Frame.length); + $scope.slider_options.scale.push( + { + val: data.event.Frame[i].FrameId, + label: ' ' + }); + } + else + { + //$scope.slider_options.scale.push(' '); + } + + } + + //console.log (JSON.stringify(data)); + }) + .error(function(err) + { + NVRDataModel.log("Error retrieving detailed frame API " + JSON.stringify(err)); + NVRDataModel.displayBanner('error', [$translate.instant('kErrorFrameBanner'), $translate.instant('kErrorPleaseTryAgain')]); + }); + + oldEvent = event; + $rootScope.rand = Math.floor(Math.random() * (999999 - 111111 + 1)) + 111111; + var elem = angular.element(document.getElementById("item-" + ndx)); + var locobject = $ionicPosition.offset(elem); + //console.log(JSON.stringify(locobject)); + var toplocation = parseInt(locobject.top); + var objheight = parseInt(locobject.height); + // console.log("top location is " + toplocation); + var distdiff = parseInt($rootScope.devHeight) - toplocation - objheight; + // console.log("*****Space at bottom is " + distdiff); + + if (distdiff < eventsListScrubHeight) // size of the scroller with bars + { + scrollbynumber = eventsListScrubHeight - distdiff; + $ionicScrollDelegate.$getByHandle("mainScroll").scrollBy(0, scrollbynumber, true); + + // we need to scroll up to make space + } + + } // end of groupType == scrub + } // end of ShowScrub == true + else + { + // $ionicScrollDelegate.freezeScroll(false); + // + // $ionicListDelegate.canSwipeItems(true); + // NVRDataModel.debug ("enabling options swipe"); + + $ionicSideMenuDelegate.canDragContent(true); + event.Event.height = eventsListDetailsHeight; + $ionicScrollDelegate.resize(); + + if (scrollbynumber) + { + $ionicScrollDelegate.$getByHandle("mainScroll").scrollBy(0, -1 * scrollbynumber, true); + scrollbynumber = 0; + } + // we are turning off, so scroll by back + } + + } + + $scope.closeIfOpen = function(event) + { + if (event != undefined) + { + if (event.Event.ShowScrub) + toggleGroup(event); + + } + }; + + $scope.isGroupShown = function(event) + { + // console.log ("IS SHOW INDEX is " + ndx); + //console.log ("SHOW GROUP IS " + showGroup); + + return (event == undefined) ? false : event.Event.ShowScrub; + + }; + + //--------------------------------------------------- + // reload view + //--------------------------------------------------- + $scope.reloadView = function() + { + // All we really need to do here is change the random token + // in the image src and it will refresh. No need to reload the view + // and if you did reload the view, it would go back to events list + // which is the view - and when you are in the modal it will go away + //console.log("*** Refreshing Modal view ***"); + //$state.go($state.current, {}, {reload: true}); + $rootScope.rand = Math.floor(Math.random() * (999999 - 111111 + 1)) + 111111; + $ionicLoading.show( + { + template: $translate.instant('kRefreshedView'), + noBackdrop: true, + duration: 3000 + }); + + }; + + //--------------------------------------------------- + // when you tap a list entry - to break search loop + //--------------------------------------------------- + $scope.tapped = function() + { + // console.log("*** TAPPED ****"); + // if he tapped, the we are not infinite loading on ion-infinite + if (enableLoadMore == false) + { + moreEvents = true; + enableLoadMore = true; + // console.log("REMOVING ARTIFICAL LOAD MORE BLOCK"); + } + }; + + $scope.$on('$ionicView.loaded', function() + { + // console.log("**VIEW ** Events Ctrl Loaded"); + }); + + //------------------------------------------------------------------------- + // Lets make sure we set screen dim properly as we enter + // The problem is we enter other states before we leave previous states + // from a callback perspective in ionic, so we really can't predictably + // reset power state on exit as if it is called after we enter another + // state, that effectively overwrites current view power management needs + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + // console.log("**VIEW ** Events Ctrl Entered"); + NVRDataModel.setAwake(false); + + EventServer.sendMessage('push', + { + type: 'badge', + badge: 0, + }); + + $ionicPopover.fromTemplateUrl('templates/events-popover.html', + { + scope: $scope, + }).then(function(popover) + { + $scope.popover = popover; + }); + + //reset badge count + if (window.cordova && window.cordova.plugins.notification) + { + $cordovaBadge.set(0).then(function() + { + // You have permission, badge set. + }, function(err) + { + NVRDataModel.debug("app does not have badge permissions. Please check your phone notification settings"); + // You do not have permission. + }); + + $cordovaLocalNotification.clearAll(); + } + + }); + + $scope.$on('$ionicView.leave', function() + { + //console.log("**VIEW ** Events Ctrl Left"); + }); + + $scope.$on('$ionicView.unloaded', function() + { + //console.log("**VIEW ** Events Ctrl Unloaded"); + //console.log("*** MODAL ** Destroying modal too"); + if ($scope.modal !== undefined) + { + $scope.modal.remove(); + } + + }); + + //--------------------------------------------------- + // used to hide loading image toast + //--------------------------------------------------- + $scope.finishedLoadingImage = function(ndx) + { + // console.log("*** Events image FINISHED loading index: "+ndx+"***"); + $ionicLoading.hide(); + }; + + //--------------------------------------------------- + // + //--------------------------------------------------- + $scope.clearSearch = function() + { + $scope.search.text = ""; + }; + + //--------------------------------------------------- + // Called when user toggles search + //--------------------------------------------------- + $scope.searchClicked = function() + { + $scope.showSearch = !$scope.showSearch; + // this helps greatly in repeat scroll gets + if ($scope.showSearch == false) + $scope.search.text = ""; + + //console.log("**** Setting search view to " + $scope.showSearch + " ****"); + if (enableLoadMore == false && $scope.showSearch == false) + { + moreEvents = true; + enableLoadMore = true; + //console.log("REMOVING ARTIFICAL LOAD MORE BLOCK"); + } + }; + + //-------------------------------------------------------- + // utility function + //-------------------------------------------------------- + + function computeRelativePath(event) + { + var relativePath = ""; + var loginData = NVRDataModel.getLogin(); + var str = event.Event.StartTime; + var yy = moment(str).locale('en').format('YY'); + var mm = moment(str).locale('en').format('MM'); + var dd = moment(str).locale('en').format('DD'); + var hh = moment(str).locale('en').format('HH'); + var min = moment(str).locale('en').format('mm'); + var sec = moment(str).locale('en').format('ss'); + relativePath = event.Event.MonitorId + "/" + + yy + "/" + + mm + "/" + + dd + "/" + + hh + "/" + + min + "/" + + sec + "/"; + return relativePath; + + } + + //-------------------------------------------------------- + // utility function + //-------------------------------------------------------- + + function computeBasePath(event) + { + var basePath = ""; + var loginData = NVRDataModel.getLogin(); + var str = event.Event.StartTime; + var yy = moment(str).locale('en').format('YY'); + var mm = moment(str).locale('en').format('MM'); + var dd = moment(str).locale('en').format('DD'); + var hh = moment(str).locale('en').format('HH'); + var min = moment(str).locale('en').format('mm'); + var sec = moment(str).locale('en').format('ss'); + + basePath = event.Event.baseURL + "/events/" + + event.Event.MonitorId + "/" + + yy + "/" + + mm + "/" + + dd + "/" + + hh + "/" + + min + "/" + + sec + "/"; + return basePath; + } + + $scope.modalGraph = function() + { + $ionicModal.fromTemplateUrl('templates/events-modalgraph.html', + { + scope: $scope, // give ModalCtrl access to this scope + animation: 'slide-in-up', + id: 'modalgraph', + + }) + .then(function(modal) + { + $scope.modal = modal; + + $scope.modal.show(); + + }); + }; + + $scope.analyzeEvent = function(ev) + { + $scope.event = ev; + $ionicModal.fromTemplateUrl('templates/timeline-modal.html', + { + scope: $scope, // give ModalCtrl access to this scope + animation: 'slide-in-up', + id: 'analyze', + }) + .then(function(modal) + { + $scope.modal = modal; + + $scope.modal.show(); + + }); + }; + + $scope.$on('modal.removed', function(e, m) + { + + if (m.id != 'footage') + return; + NVRDataModel.debug("Rebinding watchers of eventCtrl"); + setupWatchers(); + + //console.log ("************** FOOTAGE CLOSED"); + + }); + + //-------------------------------------------------------- + //This is called when we first tap on an event to see + // the feed. It's important to instantiate ionicModal here + // as otherwise you'd instantiate it when the view loads + // and our "Please wait loading" technique I explained + //earlier won't work + //-------------------------------------------------------- + + $scope.openModal = function(event) + { + + NVRDataModel.debug("unbinding eventCtrl watchers as modal has its own"); + ionRangeWatcher(); + mycarouselWatcher(); + //NVRDataModel.debug("EventCtrl: Open Modal with Base path " + relativepath); + + $scope.event = event; + + NVRDataModel.setAwake(NVRDataModel.getKeepAwake()); + + $scope.currentEvent = event; + $scope.followSameMonitor = ($stateParams.id == "0") ? "0" : "1"; + + $ionicModal.fromTemplateUrl('templates/events-modal.html', + { + scope: $scope, + animation: 'slide-in-up', + id: 'footage', + }) + .then(function(modal) + { + $scope.modal = modal; + + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + "...", + noBackdrop: true, + duration: 10000 + }); + + $scope.modal.show(); + + var ld = NVRDataModel.getLogin(); + + }); + + }; + + //-------------------------------------------------------- + //We need to destroy because we are instantiating + // it on open + //-------------------------------------------------------- + $scope.closeModal = function() + { + NVRDataModel.debug(">>>EventCtrl:Close & Destroy Modal"); + NVRDataModel.setAwake(false); + if ($scope.modal !== undefined) + { + $scope.modal.remove(); + } + + }; + + //-------------------------------------------------------- + //Cleanup the modal when we're done with it + // I Don't think it ever comes here + //-------------------------------------------------------- + $scope.$on('$destroy', function() + { + //console.log("Destroy Modal"); + if ($scope.modal !== undefined) + { + $scope.modal.remove(); + } + if ($scope.popover !== undefined) + $scope.popover.remove(); + }); + + //-------------------------------------------------------- + // used by infinite scrolling to see if we can get more + //-------------------------------------------------------- + + $scope.moreDataCanBeLoaded = function() + { + return moreEvents; + }; + + //-------------------------------------------------------- + // stop searching for more data + //-------------------------------------------------------- + $scope.cancelSearch = function() + { + $ionicLoading.hide(); //Or whatever action you want to preform + enableLoadMore = false; + //console.log("**** CANCELLED ****"); + $ionicLoading.show( + { + template: $translate.instant('kSearchCancelled'), + animation: 'fade-in', + showBackdrop: true, + duration: 2000, + maxWidth: 200, + showDelay: 0 + }); + + }; + + //-------------------------------------------------------- + // loads next page of events + //-------------------------------------------------------- + + function loadMore() + { + // the events API does not return an error for anything + // except greater page limits than reported + + // console.log("***** LOADING MORE INFINITE SCROLL ****"); + eventsPage--; + if ((eventsPage <= 0) && (pageLoaded)) + { + moreEvents = false; + //console.log("*** At Page " + eventsPage + ", not proceeding"); + return; + } + + if (!enableLoadMore) + { + moreEvents = false; // Don't ion-scroll till enableLoadMore is true; + $scope.$broadcast('scroll.infiniteScrollComplete'); + + // console.log("**** LOADMORE ARTIFICALLY DISABLED"); + return; + } + + var loadingStr = ""; + if ($scope.search.text != "") + { + var toastStr = $translate.instant('kToastSearchingPage') + eventsPage; + $ionicLoading.show( + { + maxwidth: 100, + scope: $scope, + template: '<button class="button button-clear icon-left ion-close-circled button-text-wrap" ng-click="cancelSearch()" >' + toastStr + '</button>' + }); + + loadingStr = "none"; + } + + nolangFrom = ""; + nolangTo = ""; + if ($rootScope.fromString) + nolangFrom = moment($rootScope.fromString).locale('en').format("YYYY-MM-DD HH:mm:ss"); + if ($rootScope.toString) + nolangTo = moment($rootScope.toString).locale('en').format("YYYY-MM-DD HH:mm:ss"); + + NVRDataModel.getEvents($scope.id, eventsPage, loadingStr, nolangFrom, nolangTo) + .then(function(data) + { + var loginData = NVRDataModel.getLogin(); + // console.log("Got new page of events with Page=" + eventsPage); + var myevents = data; + + for (var i = 0; i < myevents.length; i++) + { + + var idfound = true; + var ld = NVRDataModel.getLogin(); + + if (ld.persistMontageOrder) + { + idfound = false; + for (var ii = 0; ii < $scope.monitors.length; ii++) + { + if ($scope.monitors[ii].Monitor.Id == myevents[i].Event.MonitorId && (NVRDataModel.isNotHidden(myevents[i].Event.MonitorId) || showHiddenMonitors)) + { + + //console.log ( $scope.monitors[ii].Monitor.Id + " MATCHES " + myevents[i].Event.MonitorId); + idfound = true; + + break; + } + } + } + + myevents[i].Event.humanizeTime = humanizeTime(myevents[i].Event.StartTime); + myevents[i].Event.MonitorName = NVRDataModel.getMonitorName(myevents[i].Event.MonitorId); + // now construct base path + + myevents[i].Event.streamingURL = NVRDataModel.getStreamingURL(myevents[i].Event.MonitorId); + myevents[i].Event.baseURL = NVRDataModel.getBaseURL(myevents[i].Event.MonitorId); + myevents[i].Event.imageMode = NVRDataModel.getImageMode(myevents[i].Event.MonitorId); + // console.log ("***** MULTISERVER STREAMING URL FOR EVENTS " + myevents[i].Event.streamingURL); + + // console.log ("***** MULTISERVER BASE URL FOR EVENTS " + myevents[i].Event.baseURL); + + myevents[i].Event.ShowScrub = false; + myevents[i].Event.BasePath = computeBasePath(myevents[i]); + myevents[i].Event.relativePath = computeRelativePath(myevents[i]); + myevents[i].Event.height = eventsListDetailsHeight; + + if (myevents[i].Event.imageMode == 'path') + //if (1) + myevents[i].Event.videoPath = myevents[i].Event.baseURL + "/events/" + myevents[i].Event.relativePath + myevents[i].Event.DefaultVideo; + else + myevents[i].Event.videoPath = myevents[i].Event.baseURL + "/index.php?view=view_video&eid=" + myevents[i].Event.Id; + + if (idfound) $scope.events.push(myevents[i]); + } + + //console.log("Got new page of events"); + moreEvents = true; + $scope.$broadcast('scroll.infiniteScrollComplete'); + }, + + function(error) + { + // console.log("*** No More Events to Load, Stop Infinite Scroll ****"); + moreEvents = false; + $scope.$broadcast('scroll.infiniteScrollComplete'); + + }); + } + + $scope.loadMore = function() + { + loadMore(); + + }; + + $scope.toggleMinAlarmFrameCount = function() + { + + var ld = NVRDataModel.getLogin(); + + console.log("Toggling " + ld.enableAlarmCount); + ld.enableAlarmCount = !ld.enableAlarmCount; + NVRDataModel.setLogin(ld); + $scope.loginData = NVRDataModel.getLogin(); + doRefresh(); + }; + + //-------------------------------------- + // formats events dates in a nice way + //--------------------------------------- + + function humanizeTime(str) + { + //console.log ("Time:"+str+" TO LOCAL " + moment(str).local().toString()); + //if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).fromNow(); + // else + // return moment(str).fromNow(); + + } + + $scope.prettifyDate = function(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format('MMM Do'); + else + return moment(str).format('MMM Do'); + }; + + function prettifyDate(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format('MMM Do'); + else + return moment(str).format('MMM Do'); + } + + $scope.prettifyTime = function(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format(NVRDataModel.getTimeFormat()); + else + return moment(str).format(NVRDataModel.getTimeFormat()); + }; + + $scope.prettifyTimeSec = function(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format(NVRDataModel.getTimeFormatSec()); + else + return moment(str).format(NVRDataModel.getTimeFormatSec()); + }; + + $scope.prettify = function(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format(NVRDataModel.getTimeFormat() + ', MMMM Do YYYY'); + else + return moment(str).format(NVRDataModel.getTimeFormat() + ', MMMM Do YYYY'); + }; + //-------------------------------------------------------- + // For consistency we are keeping the refresher list + // but its a dummy. The reason I deviated is because + // refresh with infinite scroll is a UX problem - its + // easy to pull to refresh when scrolling up with + // a large list + //-------------------------------------------------------- + + $scope.dummyDoRefresh = function() + { + $scope.$broadcast('scroll.refreshComplete'); + }; + + $scope.doRefresh = function() + { + doRefresh(); + }; //dorefresh + + function doRefresh() + { + // console.log("***Pull to Refresh"); + + NVRDataModel.debug("Reloading monitors"); + var refresh = NVRDataModel.getMonitors(1); + refresh.then(function(data) + { + $scope.monitors = data; + + /* var ld = NVRDataModel.getLogin(); + if (ld.persistMontageOrder) { + var tempMon = data; + $scope.monitors = NVRDataModel.applyMontageMonitorPrefs(tempMon, 2)[0]; + } else { + $scope.monitors = data; + }*/ + + getInitialEvents(); + moreEvents = true; + + }); + } + +}]); diff --git a/www/js/EventDateTimeFilterCtrl.js b/www/js/EventDateTimeFilterCtrl.js new file mode 100644 index 00000000..772b16be --- /dev/null +++ b/www/js/EventDateTimeFilterCtrl.js @@ -0,0 +1,138 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console,moment */ + +angular.module('zmApp.controllers') + .controller('zmApp.EventDateTimeFilterCtrl', ['$scope', '$ionicSlideBoxDelegate', '$ionicSideMenuDelegate', '$rootScope', '$ionicHistory', 'NVRDataModel', '$state', function($scope, $ionicScrollDelegate, $ionicSideMenuDelegate, $rootScope, $ionicHistory, NVRDataModel, $state) + { + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + $scope.$on('$ionicView.beforeEnter', function() + { + $scope.today = moment().format("YYYY-MM-DD"); + }); + + //-------------------------------------------------------------------------- + // Clears filters + //-------------------------------------------------------------------------- + + $scope.removeFilters = function() + { + $rootScope.isEventFilterOn = false; + $rootScope.fromDate = ""; + $rootScope.fromTime = ""; + $rootScope.toDate = ""; + $rootScope.toTime = ""; + $rootScope.fromString = ""; + $rootScope.toString = ""; + + // if you come here via the events pullup + // you are looking at a specific monitor ID + // going back will only retain that monitor ID + // so lets reload with all monitors + // + //console.log (">>> BACKVIEW="+$ionicHistory.backTitle()); + + if ($ionicHistory.backTitle() == 'Timeline') + { + $ionicHistory.goBack(); + } + else // in events, backview is undefined? + { + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }); + return; + } + + //$ionicHistory.goBack(); + }; + + //-------------------------------------------------------------------------- + // Saves filters in root variables so EventFilter can access it. I know: + // don't root. + //-------------------------------------------------------------------------- + $scope.saveFilters = function() + { + if (!$rootScope.fromDate) + { + //console.log("RESET fromDate"); + $rootScope.fromDate = new Date(); + NVRDataModel.debug("DateTimeFilter: resetting from date"); + } + + if (!$rootScope.toDate) + { + // console.log("RESET toDate"); + $rootScope.toDate = new Date(); + NVRDataModel.debug("DateTimeFilter: resetting to date"); + } + + if (!$rootScope.fromTime) + { + // console.log("RESET fromTime"); + $rootScope.fromTime = new Date(99, 5, 24, 0, 0, 0, 0); //moment().format("hh:mm:ss"); + NVRDataModel.debug("DateTimeFilter: resetting from time"); + } + + if (!$rootScope.toTime) + { + //console.log("RESET toTime"); + $rootScope.toTime = new Date(99, 5, 24, 23, 59, 59, 0); + //$rootScope.toTime = "01:01:02"; //moment().format("hh:mm:ss"); + NVRDataModel.debug("DateTimeFilter: resetting to time"); + } + + if ($rootScope.fromDate > $rootScope.toDate) + { + NVRDataModel.log("From date > To Date, swapping"); + var t = $rootScope.fromDate; + $rootScope.fromDate = $rootScope.toDate; + $rootScope.toDate = t; + } + + $rootScope.isEventFilterOn = true; + $rootScope.fromString = moment($rootScope.fromDate).format("YYYY-MM-DD") + " " + moment($rootScope.fromTime).format("HH:mm:ss"); + + $rootScope.toString = moment($rootScope.toDate).format("YYYY-MM-DD") + " " + moment($rootScope.toTime).format("HH:mm:ss"); + + //console.log("CONCAT DATES " + temp); + // + // var startDate = moment(temp).format("YYYY-MM-DD hh:mm:ss"); + NVRDataModel.debug("DateTimeFilter: From/To is now: " + $rootScope.fromString + " & " + $rootScope.toString); + $ionicHistory.goBack(); + }; + + } + + ]); diff --git a/www/js/EventModalCtrl.js b/www/js/EventModalCtrl.js new file mode 100644 index 00000000..b8a7acbc --- /dev/null +++ b/www/js/EventModalCtrl.js @@ -0,0 +1,1873 @@ +// Common Controller for the montage view +/* jshint -W041 */ +/* jslint browser: true*/ +/* global saveAs, cordova,StatusBar,angular,console,ionic, moment, Chart */ + +angular.module('zmApp.controllers').controller('EventModalCtrl', ['$scope', '$rootScope', 'zm', 'NVRDataModel', '$ionicSideMenuDelegate', '$timeout', '$interval', '$ionicModal', '$ionicLoading', '$http', '$state', '$stateParams', '$ionicHistory', '$ionicScrollDelegate', '$q', '$sce', 'carouselUtils', '$ionicPopup', '$translate', '$filter', 'SecuredPopups', function($scope, $rootScope, zm, NVRDataModel, $ionicSideMenuDelegate, $timeout, $interval, $ionicModal, $ionicLoading, $http, $state, $stateParams, $ionicHistory, $ionicScrollDelegate, $q, $sce, carouselUtils, $ionicPopup, $translate, $filter, SecuredPopups) +{ + + // from parent scope + var currentEvent = $scope.currentEvent; + var nphTimer; + var eventQueryHandle; + $scope.loginData = NVRDataModel.getLogin(); + $scope.currentRate = '-'; + var timeFormat = 'MM/DD/YYYY HH:mm:ss'; + var event; + var gEvent; + var handle; + + var framearray = { + + labels: [], + datasets: [ + { + //label: '# of Votes', + backgroundColor: 'rgba(242, 12, 12, 0.5)', + borderColor: 'rgba(242, 12, 12, 0.5)', + data: [], + }] + }; + + var frameoptions = []; + + var eventImageDigits = 5; // failsafe + $scope.currentProgress = { + progress: 0 + }; + $scope.sliderProgress = { + progress: 0 + }; + NVRDataModel.getKeyConfigParams(0) + .then(function(data) + { + //console.log ("***GETKEY: " + JSON.stringify(data)); + eventImageDigits = parseInt(data); + NVRDataModel.log("Image padding digits reported as " + eventImageDigits); + }); + + $scope.animationInProgress = false; + $scope.imageFit = true; + // FIXME: This is a hack - for some reason + // the custom slider view is messed up till the image loads + // in modal view + $scope.showModalRangeSider = false; + $scope.isModalActive = true; + + $timeout(function() + { + $scope.showModalRangeSider = true; + + }, 2000); + + document.addEventListener("pause", onPause, false); + document.addEventListener("resume", onResume, false); + + $rootScope.authSession = "undefined"; + $ionicLoading.show( + { + template: $translate.instant('kNegotiatingStreamAuth'), + animation: 'fade-in', + showBackdrop: true, + duration: zm.loadingTimeout, + maxWidth: 300, + showDelay: 0 + }); + var ld = NVRDataModel.getLogin(); + + $scope.currentStreamMode = ld.gapless ? 'gapless' : 'single'; + NVRDataModel.log("Using stream mode " + $scope.currentStreamMode); + + NVRDataModel.debug("EventModalCtrl called from " + $ionicHistory.currentStateName()); + // This is not needed for event mode + + NVRDataModel.debug("Setting playback to " + $scope.streamMode); + + $rootScope.validMonitorId = $scope.monitors[0].Monitor.Id; + NVRDataModel.getAuthKey($rootScope.validMonitorId, (Math.floor((Math.random() * 999999) + 1)).toString()) + .then(function(success) + { + $ionicLoading.hide(); + $rootScope.authSession = success; + NVRDataModel.log("Modal: Stream authentication construction: " + $rootScope.authSession); + + }, + function(error) + { + + $ionicLoading.hide(); + NVRDataModel.debug("ModalCtrl: Error details of stream auth:" + error); + //$rootScope.authSession=""; + NVRDataModel.log("Modal: Error returned Stream authentication construction. Retaining old value of: " + $rootScope.authSession); + }); + + //-------------------------------------------------------------------------------------- + // Handles bandwidth change, if required + // + //-------------------------------------------------------------------------------------- + + $rootScope.$on("bandwidth-change", function(e, data) + { + // not called for offline, I'm only interested in BW switches + NVRDataModel.debug("Got network change:" + data); + var ds; + if (data == 'lowbw') + { + ds = $translate.instant('kLowBWDisplay'); + } + else + { + ds = $translate.instant('kHighBWDisplay'); + } + NVRDataModel.displayBanner('net', [ds]); + + var ld = NVRDataModel.getLogin(); + + $scope.singleImageQuality = (NVRDataModel.getBandwidth() == "lowbw") ? zm.eventSingleImageQualityLowBW : ld.singleImageQuality; + }); + + //------------------------------------------------------- + // we use this to reload the connkey if authkey changed + //------------------------------------------------------ + + $rootScope.$on("auth-success", function() + { + + NVRDataModel.debug("EventModalCtrl: Re-login detected, resetting everything & re-generating connkey"); + NVRDataModel.stopNetwork("Auth-Success inside EventModalCtrl"); + $scope.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + //console.log ("********* OFFSET FROM AUTH SUCC"); + $timeout(function() + { + sendCommand('14', $scope.connKey, '&offset=' + $scope.currentProgress.progress); + }, 500); + //$timeout.cancel(eventQueryHandle); + //eventQueryHandle = $timeout (function(){checkEvent();}, zm.eventPlaybackQuery); + + }); + + //------------------------------------------------------- + // tap to pause + //------------------------------------------------------ + + $scope.togglePause = function() + { + $scope.isPaused = !$scope.isPaused; + NVRDataModel.debug("Paused is " + $scope.isPaused); + sendCommand($scope.isPaused ? '1' : '2', $scope.connKey); + + }; + + $scope.onPlayerReady = function(api) + { + + // we need this timeout to avoid load interrupting + // play -- I suppose its an angular digest foo thing + console.log ("*********** ON PLAY READY"); + handle = api; + + $ionicLoading.show( + { + template: "<ion-spinner icon='ripple' class='spinner-energized'></ion-spinner><br/>" + $translate.instant('kVideoLoading')+"...", + + }); + NVRDataModel.debug("Player is ready"); + $timeout(function() + { + handle.pause(); + handle.setPlayback(NVRDataModel.getLogin().videoPlaybackSpeed); + handle.play(); + NVRDataModel.debug ("*** Invoking play"); + + }, 300); + + // window.stop(); + }; + + $scope.onPlaybackUpdate = function(rate) + { + console.log ("UPDATED RATE TO "+rate); + var ld = NVRDataModel.getLogin(); + ld.videoPlaybackSpeed = rate; + NVRDataModel.setLogin(ld); + }; + + $scope.onCanPlay = function() + { + + console.log ("*********** CAN PLAY"); + $ionicLoading.hide(); + NVRDataModel.debug("This video can be played"); + $scope.videoObject.config.cuepoints.points = []; + // now set up cue points + NVRDataModel.debug("Setting cue points.."); + NVRDataModel.debug ("API-Total length:"+currentEvent.Event.Length); + NVRDataModel.debug ("Player-Total length:"+handle.totalTime/1000); + + for (var l=0; l<currentEvent.Frame.length; l++ ) + { + if (currentEvent.Frame[l].Type=='Alarm') + { + // var ft = moment(currentEvent.Frame[l].TimeStamp); + //var s = factor*Math.abs(st.diff(ft,'seconds')); + + var s = currentEvent.Frame[l].Delta; + + //console.log("START="+currentEvent.Event.StartTime); + //console.log("END="+currentEvent.Frame[l].TimeStamp); + NVRDataModel.debug ("alarm cue at:"+s+"s"); + $scope.videoObject.config.cuepoints.points.push({time:s}); + } + } + }; + + $scope.onVideoError = function(event) + { + $ionicLoading.hide(); + if (!$scope.isModalActive) return; + NVRDataModel.debug("player reported a video error:" + JSON.stringify(event)); + $rootScope.zmPopup = SecuredPopups.show('alert', + { + title: $translate.instant('kError'), + template: $rootScope.platformOS == 'desktop' ? $translate.instant('kVideoError') : $translate.instant('kVideoErrorMobile'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + + }; + + //------------------------------------------------------- + // This is what we call every zm.EventQueryInterval + // it really only queries to get status to it can display + // zms takes care of the display + //------------------------------------------------------ + + function checkEvent() + { + + if ($scope.modalFromTimelineIsOpen == false) + { + NVRDataModel.log("Modal was closed in timeline, cancelling timer"); + $interval.cancel(eventQueryHandle); + return; + } + + //console.log ("Event timer"); + //console.log ("Event timer"); + $scope.checkEventOn = true; + if ($scope.defaultVideo !== undefined && $scope.defaultVideo != '') + { + //console.log("playing video, not using zms, skipping event commands"); + } + else + { + processEvent('99', $scope.connKey); + } + } + + function sendCommand(cmd, connkey, extras, rq) + { + var d = $q.defer(); + + if ($scope.defaultVideo !== undefined && $scope.defaultVideo != '') + { + // console.log("playing video, not using zms, skipping event commands"); + d.resolve(true); + return (d.promise); + } + + var loginData = NVRDataModel.getLogin(); + //console.log("Sending CGI command to " + loginData.url); + var rqtoken = rq ? rq : "stream"; + var myauthtoken = $rootScope.authSession.replace("&auth=", ""); + //&auth= + $http( + { + method: 'POST', + /*timeout: 15000,*/ + url: loginData.url + '/index.php', + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', + //'Accept': '*/*', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + + encodeURIComponent(obj[p])); + var foo = str.join("&"); + if (extras) + { + foo = foo + extras; + //console.log("EXTRAS****SUB RETURNING " + foo); + } + //console.log("CGI subcommand=" + foo); + return foo; + + }, + + data: + { + view: "request", + request: rqtoken, + connkey: connkey, + command: cmd, + auth: myauthtoken, + // user: loginData.username, + // pass: loginData.password + } + }) + .then(function(resp) + { + NVRDataModel.debug("sendCmd response:" + JSON.stringify(resp)); + d.resolve(resp); + return (d.promise); + + }, + function(resp) + { + NVRDataModel.debug("sendCmd error:" + JSON.stringify(resp)); + d.reject(resp); + return (d.promise); + }); + + return (d.promise); + } + + function processEvent(cmd, connkey) + { + + if ($scope.blockSlider) + { + //console.log("Not doing ZMS Command as slider is depressed..."); + return; + } + + var loginData = NVRDataModel.getLogin(); + //console.log("sending process Event command to " + loginData.url); + var myauthtoken = $rootScope.authSession.replace("&auth=", ""); + //&auth= + var req = $http( + { + method: 'POST', + /*timeout: 15000,*/ + url: loginData.url + '/index.php', + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', + //'Accept': '*/*', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + + encodeURIComponent(obj[p])); + var foo = str.join("&"); + //console.log("****processEvent subcommands RETURNING " + foo); + return foo; + }, + + data: + { + view: "request", + request: "stream", + connkey: connkey, + command: cmd, + auth: myauthtoken, + // user: loginData.username, + // pass: loginData.password + } + }); + + req.success(function(resp) + { + // NVRDataModel.debug ("processEvent success:"+JSON.stringify(resp)); + + if (resp.result == "Ok") + { + + $scope.currentProgress.progress = resp.status.progress; + $scope.eventId = resp.status.event; + $scope.d_eventId = $scope.eventId; + $scope.currentRate = resp.status.rate; + + if ($scope.currentProgress.progress > $scope.currentEventDuration) $scope.currentProgress.progress = $scope.currentEventDuration; + $scope.progressText = "At " + $scope.currentProgress.progress + "s of " + $scope.currentEventDuration + "s"; + + $scope.sliderProgress.progress = $scope.currentProgress.progress; + + // lets not do this and use zms to move forward or back + // as this code conflicts with fast rev etc + //if (Math.floor(resp.status.progress) >=$scope.currentEventDuration) + + //$timeout (checkEvent(), zm.eventPlaybackQuery); + //eventQueryHandle = $timeout (function(){checkEvent();}, zm.eventPlaybackQuery); + + } + else // resp.result was messed up + + { + NVRDataModel.debug("Hmm I found an error " + JSON.stringify(resp)); + //window.stop(); + $scope.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + + // console.log (JSON.stringify(resp)); + $timeout(function() + { + sendCommand('14', $scope.connKey, '&offset=' + $scope.currentProgress.progress); + }, 500); + NVRDataModel.debug("so I'm regenerating Connkey to " + $scope.connKey); + //eventQueryHandle = $timeout (function(){checkEvent();}, zm.eventPlaybackQuery); + } + }); + + req.error(function(resp) + { + NVRDataModel.debug("processEvent error:" + JSON.stringify(resp)); + //eventQueryHandle = $timeout (function(){checkEvent();}, zm.eventPlaybackQuery); + + }); + + } + + function onPause() + { + + // $interval.cancel(modalIntervalHandle); + + // FIXME: Do I need to setAwake(false) here? + $interval.cancel(eventQueryHandle); + NVRDataModel.log("EventModalCtrl: paused, killing timer"); + + } + + function onResume() + { + NVRDataModel.debug("EventModalCtrl: Modal resume called"); + $rootScope.modalRand = Math.floor((Math.random() * 100000) + 1); + + } + + $scope.finishedLoadingImage = function() + { + // console.log("***Monitor image FINISHED Loading***"); + $ionicLoading.hide(); + + }; + + $scope.enableSliderBlock = function() + { + $scope.blockSlider = true; + }; + + $scope.youChangedSlider = function() + { + + //console.log("YOU changed " + $scope.sliderProgress.progress); + $scope.currentProgress.progress = $scope.sliderProgress.progress; + sendCommand('14', $scope.connKey, '&offset=' + $scope.currentProgress.progress) + .then(function(s) + { + $scope.blockSlider = false; + }, function(e) + { + $scope.blockSlider = false; + }); + + }; + + //----------------------------------------------------------------------- + // Sucess/Error handlers for saving a snapshot of the + // monitor image to phone storage + //----------------------------------------------------------------------- + + function SaveSuccess() + { + $ionicLoading.show( + { + template: $translate.instant('kDone'), + noBackdrop: true, + duration: 1000 + }); + NVRDataModel.debug("ModalCtrl:Photo saved successfuly"); + } + + function SaveError(e) + { + $ionicLoading.show( + { + template: $translate.instant('kErrorSave'), + noBackdrop: true, + duration: 2000 + }); + NVRDataModel.log("Error saving image: " + e.message); + //console.log("***ERROR"); + } + + $scope.jumpToOffsetInEvent = function() + { + // streamReq.send( streamParms+"&command="+CMD_SEEK+"&offset="+offset ); + }; + + //----------------------------------------------------------------------- + // Saves a snapshot of the monitor image to phone storage + //----------------------------------------------------------------------- + + $scope.saveEventImageToPhoneWithPerms = function(onlyAlarms) + { + + if ($rootScope.platformOS != 'android') + { + processSaveEventImageToPhone(onlyAlarms); + return; + } + + // if we are on android do the 6.x+ hasPermissions flow + NVRDataModel.debug("EventModalCtrl: Permission checking for write"); + var permissions = cordova.plugins.permissions; + permissions.hasPermission(permissions.WRITE_EXTERNAL_STORAGE, checkPermissionCallback, null); + + function checkPermissionCallback(status) + { + if (!status.hasPermission) + { + SaveError("No permission to write to external storage"); + } + permissions.requestPermission(permissions.WRITE_EXTERNAL_STORAGE, succ, err); + } + + function succ(s) + { + processSaveEventImageToPhone(onlyAlarms); + } + + function err(e) + { + SaveError("Error in requestPermission"); + } + }; + + function processSaveEventImageToPhone(onlyAlarms) + { + + if ($scope.loginData.useNphZmsForEvents) + { + NVRDataModel.log("Use ZMS stream to save to phone"); + saveEventImageToPhoneZms(onlyAlarms); + + } + else + { + saveEventImageToPhone(onlyAlarms); + } + + } + + function saveEventImageToPhoneZms(onlyAlarms) + { + // The strategy here is to build the array now so we can grab frames + // $scope.currentProgress.progress is the seconds where we are + // $scope.eventId is the event Id + + $scope.isPaused = true; + + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait'), + noBackdrop: true, + duration: zm.httpTimeout + }); + sendCommand('1', $scope.connKey). + then(function(resp) + { + + // console.log ("PAUSE ANSWER IS " + JSON.stringify(resp)); + $scope.currentProgress.progress = resp.data.status.progress; + // console.log ("STEP 0 progress is " + $scope.currentProgress.progress); + $scope.slides = []; + + var apiurl = $scope.loginData.apiurl + "/events/" + $scope.eventId + ".json"; + NVRDataModel.debug("prepared to get frame details using " + apiurl); + $http.get(apiurl) + .then(function(success) + { + + event = success.data.event; + + event.Event.BasePath = computeBasePath(event); + event.Event.relativePath = computeRelativePath(event); + $scope.playbackURL = $scope.loginData.url; + $scope.eventBasePath = event.Event.BasePath; + $scope.relativePath = event.Event.relativePath; + + // now lets get approx frame # + + var totalTime = event.Event.Length; + var totalFrames = event.Event.Frames; + + var myFrame = Math.round(totalFrames / totalTime * $scope.currentProgress.progress); + + // console.log ("STEP 0: playback " + $scope.playbackURL + " total time " + totalTime + " frames " + totalFrames); + + if (myFrame > totalFrames) myFrame = totalFrames; + + // console.log ("STEP 0 myFrame is " + myFrame); + // console.log ("DUMPING " + JSON.stringify(event)); + $scope.mycarousel.index = myFrame; + // console.log ("STEP 1 : Computed index as "+ $scope.mycarousel.index); + var i, p = 0; + for (i = 1; i <= event.Frame.length; i++) + { + var fname = padToN(event.Frame[i - 1].FrameId, eventImageDigits) + "-capture.jpg"; + // console.log ("Building " + fname); + + // console.log ("DUMPING ONE " + JSON.stringify(event.Frame[i-1])); + // onlyAlarms means only copy alarmed frames + if (onlyAlarms) + { + if (event.Frame[i - 1] && event.Frame[i - 1].Type == 'Alarm') + { + p++; + $scope.slides.push( + { + id: event.Frame[i - 1].FrameId, + img: fname, + }); + //console.log ("ALARM PUSHED " + fname); + } + } + else // push all frames + { + //now handle bulk frames pushing before pushing this one + if (event.Frame[i-1].Type == 'Bulk') + { + var f1 = parseInt(event.Frame[i-2].FrameId); + var f2 = parseInt(event.Frame[i-1].FrameId); + + //console.log ("Filling in bulk from:"+f1+" to "+(f2-1)); + for (var bulk=f1+1; bulk < f2; bulk++) + { + //console.log ("Storing bulk:"+bulk); + var bfname = padToN(bulk, eventImageDigits) + "-capture.jpg"; + p++; + $scope.slides.push({ + id: bulk, + img: bfname + + }); + + + } + } + //console.log ("storing: "+event.Frame[i - 1].FrameId); + p++; + $scope.slides.push( + { + id: event.Frame[i - 1].FrameId, + img: fname, + }); + + + + } + + } + //console.log ("I PUSHED:" + p+" BUT SLIDE LENGHT BEFORE DISPLAY:"+$scope.slides.length); + // console.log ("STEP 2 : calling Save Event To Phone"); + $ionicLoading.hide(); + saveEventImageToPhone(onlyAlarms); + + }, + function(err) + { + $ionicLoading.hide(); + NVRDataModel.log("snapshot API Error: Could not get frames " + JSON.stringify(err)); + + $ionicLoading.show( + { + template: $translate.instant('kErrorRetrievingFrames'), + noBackdrop: true, + duration: 4000 + }); + }); + }, + + function(err) + { + NVRDataModel.debug("Error pausing stream before snapshot " + JSON.stringify(err)); + $ionicLoading.hide(); + } + + ); // then + + } + + // don't think this is used anymore + function saveEventImageToPhone(onlyAlarms) + { + // console.log ("________________UNUSED?_______________________"); + var curState = carouselUtils.getStop(); + carouselUtils.setStop(true); + + //console.log("Your index is " + $scope.mycarousel.index); + //console.log("Associated image is " + $scope.slides[$scope.mycarousel.index].img); + + NVRDataModel.debug("ModalCtrl: SaveEventImageToPhone called"); + var canvas, context, imageDataUrl, imageData; + var loginData = NVRDataModel.getLogin(); + + // for alarms only + if (onlyAlarms) $scope.mycarousel.index = 0; + var url = $scope.playbackURL + '/index.php?view=image&rand=' + $rootScope.rand + "&path=" + $scope.relativePath + $scope.slides[$scope.mycarousel.index].img; + + $scope.selectEventUrl = url; + $scope.slideIndex = $scope.mycarousel.index; + $scope.slideLastIndex = $scope.slides.length - 1; + console.log ("FRAMES LENGTH IS " +$scope.slideLastIndex ); + + // console.log ("URL TO DISPLAY " + url); + + $rootScope.zmPopup = $ionicPopup.show( + { + template: '<center>Frame: {{slideIndex+1}} / {{slideLastIndex+1}}</center><br/><img src="{{selectEventUrl}}" width="100%" />', + title: 'Select ' + (onlyAlarms ? 'Alarmed ' : '') + 'frame to save', + subTitle: 'use left and right arrows to change', + scope: $scope, + cssClass: 'popup95', + buttons: [ + { + // left 1 + text: '', + type: 'button-small button-energized ion-chevron-left', + onTap: function(e) + { + if ($scope.slideIndex > 0) $scope.slideIndex--; + $scope.selectEventUrl = $scope.playbackURL + '/index.php?view=image&rand=' + $rootScope.rand + "&path=" + $scope.relativePath + $scope.slides[$scope.slideIndex].img; + //NVRDataModel.log("selected frame is " + $scope.slideIndex); + + console.log("URL TO DISPLAY " + $scope.slides[$scope.slideIndex].img); + + e.preventDefault(); + } + }, + { + // right 1 + text: '', + type: 'button-small button-energized ion-chevron-right', + onTap: function(e) + { + if ($scope.slideIndex < $scope.slideLastIndex) $scope.slideIndex++; + $scope.selectEventUrl = $scope.playbackURL + '/index.php?view=image&rand=' + $rootScope.rand + "&path=" + $scope.relativePath + $scope.slides[$scope.slideIndex].img; + //NVRDataModel.log("selected frame is " + $scope.slideIndex); + console.log("URL TO DISPLAY " + $scope.slides[$scope.slideIndex].img); + e.preventDefault(); + } + }, + { + // left 10 + text: '', + type: 'button-small button-energized ion-skip-backward', + onTap: function(e) + { + var tempVar = $scope.slideIndex; + tempVar -= 10; + if (tempVar < 0) tempVar = 0; + $scope.slideIndex = tempVar; + + $scope.selectEventUrl = $scope.playbackURL + '/index.php?view=image&rand=' + $rootScope.rand + "&path=" + $scope.relativePath + $scope.slides[$scope.slideIndex].img; + //NVRDataModel.log("selected frame is " + $scope.slideIndex); + + e.preventDefault(); + } + }, + { + // right 10 + text: '', + type: 'button-small button-energized ion-skip-forward', + onTap: function(e) + { + var tempVar = $scope.slideIndex; + tempVar += 10; + if (tempVar > $scope.slideLastIndex) tempVar = $scope.slideLastIndex; + $scope.slideIndex = tempVar; + if ($scope.slideIndex < $scope.slideLastIndex) $scope.slideIndex++; + $scope.selectEventUrl = $scope.playbackURL + '/index.php?view=image&rand=' + $rootScope.rand + "&path=" + $scope.relativePath + $scope.slides[$scope.slideIndex].img; + //NVRDataModel.log("selected frame is " + $scope.slideIndex); + e.preventDefault(); + } + }, + + { + text: '', + type: 'button-assertive button-small ion-close-round' + }, + { + text: '', + type: 'button-positive button-small ion-checkmark-round', + onTap: function(e) + { + saveNow(); + + } + } + ] + }); + + function saveNow() + { + $ionicLoading.show( + { + template: $translate.instant('kSavingSnapshot') + "...", + noBackdrop: true, + duration: zm.httpTimeout + }); + var url = $scope.selectEventUrl; + NVRDataModel.log("saveNow: File path to grab is " + url); + + var img = new Image(); + img.onload = function() + { + // console.log("********* ONLOAD"); + canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + context = canvas.getContext('2d'); + context.drawImage(img, 0, 0); + + imageDataUrl = canvas.toDataURL('image/jpeg', 1.0); + imageData = imageDataUrl.replace(/data:image\/jpeg;base64,/, ''); + + if ($rootScope.platformOS != "desktop") + { + try + { + + cordova.exec( + SaveSuccess, + SaveError, + 'Canvas2ImagePlugin', + 'saveImageDataToLibrary', [imageData] + ); + // carouselUtils.setStop(curState); + } + catch (e) + { + + SaveError(e.message); + // carouselUtils.setStop(curState); + } + } + else + { + + var fname = $scope.relativePath + $scope.slides[$scope.slideIndex].img + ".png"; + fname = fname.replace(/\//, "-"); + fname = fname.replace(/\.jpg/, ''); + + canvas.toBlob(function(blob) + { + saveAs(blob, fname); + SaveSuccess(); + }); + } + }; + try + { + img.src = url; + // console.log ("SAVING IMAGE SOURCE"); + } + catch (e) + { + SaveError(e.message); + } + } + } + + $scope.reloadView = function() + { + NVRDataModel.log("Reloading view for modal view, recomputing rand"); + $rootScope.modalRand = Math.floor((Math.random() * 100000) + 1); + $scope.isModalActive = true; + }; + + $scope.scaleImage = function() + { + + $scope.imageFit = !$scope.imageFit; + console.log("Switching image style to " + $scope.imageFit); + }; + + $scope.$on('$ionicView.enter', function() + { + //console.log (">>>>>>>>>>>>>>>>>>>> MODAL VIEW ENTER"); + + }); + + $scope.$on('modal.shown', function(e, m) + { + + if (m.id != 'footage') + + return; + + $scope.isToggleListMenu = true; + $scope.videoDynamicTime = ""; + $scope.videoIsReady = false; + var ld = NVRDataModel.getLogin(); + $scope.loginData = NVRDataModel.getLogin(); + + $scope.singleImageQuality = (NVRDataModel.getBandwidth() == "lowbw") ? zm.eventSingleImageQualityLowBW : ld.singleImageQuality; + $scope.blockSlider = false; + $scope.checkEventOn = false; + //$scope.singleImageQuality = 100; + + //$scope.commandURL = $scope.currentEvent.Event.baseURL+"/index.php"; + // NVRDataModel.log (">>>>>>>>>>>>>>>>>>ZMS url command is " + $scope.commandURL); + + currentEvent = $scope.currentEvent; + + //console.log("Current Event " + JSON.stringify(currentEvent)); + $scope.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + NVRDataModel.debug("Generated Connkey:" + $scope.connKey); + + $scope.currentFrame = 1; + $scope.isPaused = false; + + gEvent = $scope.currentEvent; + //console.log ("CURRENT EVENT " + JSON.stringify($scope.currentEvent)); + // + $scope.currentEventDuration = Math.floor($scope.currentEvent.Event.Length); + //console.log ($scope.event.Event.Frames); + if (currentEvent && currentEvent.Event) + { + //console.log ("************ CALLING PREPARE MODAL ***********"); + prepareModalEvent(currentEvent.Event.Id); + if (ld.useNphZmsForEvents) + { + $timeout(function() + { + + if ($scope.modal != undefined && $scope.modal.isShown()) + { + NVRDataModel.log(">>>Starting checkAllEvents interval..."); + + //eventQueryHandle = $timeout (checkEvent(), zm.eventPlaybackQuery); + + $interval.cancel(eventQueryHandle); + eventQueryHandle = $interval(function() + { + checkEvent(); + // console.log ("Refreshing Image..."); + }.bind(this), (NVRDataModel.getBandwidth() == "lowbw") ? zm.eventPlaybackQueryLowBW : zm.eventPlaybackQuery); + } + else + { + NVRDataModel.log(">>>Modal was exited, not starting checkAllEvents"); + } + + }, 5000); + } + + } + + }); + + //var current_data; + function drawGraph() + { + + var cv = document.getElementById("eventchart"); + var ctx = cv.getContext("2d"); + + frameoptions = { + responsive: true, + legend: false, + title: + { + display: false, + text: "" + }, + scales: + { + yAxes: [ + { + display: false, + scaleLabel: + { + display: false, + labelString: 'value', + } + + }], + xAxes: [ + { + type: 'time', + display: false, + time: + { + + format: timeFormat, + tooltipFormat: 'll HH:mm', + min: framearray.datasets[0].data[0].x, + max: framearray.datasets[0].data[framearray.datasets[0].data.length - 1].x, + displayFormats: + { + + } + }, + scaleLabel: + { + display: false, + labelString: '' + } + + }] + } + }; + + $timeout(function() + { + + var myChart = new Chart(ctx, + { + type: 'line', + data: framearray, + options: frameoptions, + }); + + }); + } + + $scope.videoTime = function(s, c) + { + var a, o; + if (NVRDataModel.getLogin().useLocalTimeZone) + { + a = moment.tz(s, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()); + + } + else + { + a = moment(s); + } + a.add(c); + + o = a.format("MMM Do " + NVRDataModel.getTimeFormatSec()); + $scope.videoDynamicTime = o; + //return a.format("MMM Do "+o); + + }; + + $scope.$on('modal.removed', function(e, m) + { + console.log("************* REMOVE CALLED"); + $interval.cancel(eventQueryHandle); + if (m.id != 'footage') + return; + + $scope.isModalActive = false; + + NVRDataModel.debug("Modal removed - killing connkey"); + sendCommand(17, $scope.connKey); + //$timeout (function(){NVRDataModel.stopNetwork("Modal removed inside EventModalCtrl");},400); + + // Execute action + }); + + // Playback speed adjuster + $scope.adjustSpeed = function(val) + { + + if ($scope.defaultVideo !== undefined && $scope.defaultVideo != '') + { + + $ionicLoading.show( + { + template: $translate.instant('kUseVideoControls'), + noBackdrop: true, + duration: 3000 + }); + return; + } + + var ld = NVRDataModel.getLogin(); + + if (ld.useNphZmsForEvents) + { + + var cmd; + $scope.isPaused = false; + switch (val) + { + case 'ff': + cmd = 4; + break; + case 'fr': + cmd = 7; + break; + case 'np': + cmd = 2; + break; + case 'p': + cmd = 1; + $scope.isPaused = true; + break; + default: + cmd = 0; + } + + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + "...", + noBackdrop: true, + duration: zm.httpTimeout + }); + + sendCommand(cmd, $scope.connKey) + .then(function(success) + { + $ionicLoading.hide(); + + }, + function(err) + { + $ionicLoading.hide(); + NVRDataModel.debug("Error in adjust speed: " + JSON.stringify(err)); + } + ); + + } + else // not using nph + { + + switch (val) + { + + case "super": + $scope.eventSpeed = 20 / $scope.event.Event.Frames; + carouselUtils.setDuration($scope.eventSpeed); + break; + case "normal": + $scope.eventSpeed = $scope.event.Event.Length / $scope.event.Event.Frames; + //$scope.eventSpeed = 5; + carouselUtils.setDuration($scope.eventSpeed); + + break; + case "faster": + $scope.eventSpeed = $scope.eventSpeed / 2; + if ($scope.eventSpeed < 20 / $scope.event.Event.Frames) + $scope.eventSpeed = 10 / $scope.event.Event.Frames; + carouselUtils.setDuration($scope.eventSpeed); + break; + case "slower": + $scope.eventSpeed = $scope.eventSpeed * 2; + carouselUtils.setDuration($scope.eventSpeed); + + break; + default: + + } + NVRDataModel.debug("Set playback speed to " + $scope.eventSpeed); + + $ionicLoading.show( + { + template: $translate.instant('kPlaybackInterval') + ': ' + $scope.eventSpeed.toFixed(3) + "ms", + animation: 'fade-in', + showBackdrop: false, + duration: 1500, + maxWidth: 300, + showDelay: 0 + }); + } + + }; + + $scope.toggleListMenu = function() + { + + $scope.isToggleListMenu = !$scope.isToggleListMenu; + }; + + $scope.toggleGapless = function() + { + // console.log(">>>>>>>>>>>>>>GAPLESS TOGGLE INSIDE MODAL"); + $scope.loginData.gapless = !$scope.loginData.gapless; + NVRDataModel.setLogin($scope.loginData); + + NVRDataModel.debug("EventModalCtrl: gapless has changed resetting everything & re-generating connkey"); + NVRDataModel.stopNetwork("EventModalCtrl-toggle gapless"); + NVRDataModel.debug("Regenerating connkey as gapless has changed"); + // console.log ("********* OFFSET FROM TOGGLE GAPLESS"); + $scope.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + $timeout(function() + { + sendCommand('14', $scope.connKey, '&offset=' + $scope.currentProgress.progress); + }, 500); + //$timeout.cancel(eventQueryHandle); + //eventQueryHandle = $timeout (function(){checkEvent();}, zm.eventPlaybackQuery); + + }; + + // This function returns neighbor events if applicable + function neighborEvents(eid) + { + var d = $q.defer(); + // now get event details to show alarm frames + var loginData = NVRDataModel.getLogin(); + var myurl = loginData.apiurl + '/events/' + eid + ".json"; + var neighbors = { + prev: "", + next: "" + }; + $http.get(myurl) + .success(function(data) + { + + // In Timeline view, gapless should stick to the same monitor + if ($scope.followSameMonitor == "1") // we are viewing only one monitor + { + NVRDataModel.debug("Getting next event for same monitor Id "); + neighbors.prev = data.event.Event.PrevOfMonitor ? data.event.Event.PrevOfMonitor : ""; + neighbors.next = data.event.Event.NextOfMonitor ? data.event.Event.NextOfMonitor : ""; + } + else + { + neighbors.prev = data.event.Event.Prev ? data.event.Event.Prev : ""; + neighbors.next = data.event.Event.Next ? data.event.Event.Next : ""; + } + NVRDataModel.debug("Neighbor events of " + eid + "are Prev:" + + neighbors.prev + " and Next:" + neighbors.next); + + d.resolve(neighbors); + return (d.promise); + }) + .error(function(err) + { + NVRDataModel.log("Error retrieving neighbors" + JSON.stringify(err)); + d.reject(neighbors); + return (d.promise); + + }); + return (d.promise); + + } + + $scope.zoomImage = function(val) + { + var zl = parseInt($ionicScrollDelegate.$getByHandle("imgscroll").getScrollPosition().zoom); + if (zl == 1 && val == -1) + { + NVRDataModel.debug("Already zoomed out max"); + return; + } + + zl += val; + NVRDataModel.debug("Zoom level is " + zl); + $ionicScrollDelegate.$getByHandle("imgscroll").zoomTo(zl, true); + + }; + + //-------------------------------------------------------- + //Navigate to next/prev event in full screen mode + //-------------------------------------------------------- + + $scope.onSwipeEvent = function(eid, dirn) + { + //console.log("HERE"); + var ld = NVRDataModel.getLogin(); + if (!ld.canSwipeMonitors) return; + + if ($ionicScrollDelegate.$getByHandle("imgscroll").getScrollPosition().zoom != 1) + { + //console.log("Image is zoomed in - not honoring swipe"); + return; + } + + if (ld.useNphZmsForEvents) + { + NVRDataModel.log("using zms to move "); + jumpToEventZms($scope.connKey, dirn); + // sendCommand ( dirn==1?'13':'12',$scope.connKey); + + } + else + { + jumpToEvent(eid, dirn); + } + + //console.log("JUMPING"); + + }; + + $scope.jumpToEvent = function(eid, dirn) + { + // console.log("jumptoevent"); + var ld = NVRDataModel.getLogin(); + if (ld.useNphZmsForEvents) + { + NVRDataModel.log("using zms to move "); + jumpToEventZms($scope.connKey, dirn); + // sendCommand ( dirn==1?'13':'12',$scope.connKey); + + } + else + { + jumpToEvent(eid, dirn); + } + + }; + + function jumpToEvent(eid, dirn) + { + NVRDataModel.log("Event jump called with:" + eid); + if (eid == "") + { + $ionicLoading.show( + { + template: $translate.instant('kNoMoreEvents'), + noBackdrop: true, + duration: 2000 + }); + + return; + } + + var slidein; + var slideout; + if (dirn == 1) + { + slideout = "animated slideOutLeft"; + slidein = "animated slideInRight"; + } + else + { + slideout = "animated slideOutRight"; + slidein = "animated slideInLeft"; + } + var element = angular.element(document.getElementById("full-screen-event")); + element.addClass(slideout).one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', outWithOld); + + function outWithOld() + { + + NVRDataModel.log("ModalCtrl:Stopping network pull..."); + NVRDataModel.stopNetwork("EventModalCtrl-out with old"); + $scope.animationInProgress = true; + // give digest time for image to swap + // 100 should be enough + $timeout(function() + { + element.removeClass(slideout); + element.addClass(slidein) + .one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', inWithNew); + prepareModalEvent(eid); + }, 200); + } + + function inWithNew() + { + element.removeClass(slidein); + $scope.animationInProgress = false; + carouselUtils.setStop(false); + } + + } + + function humanizeTime(str) + { + // if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).fromNow(); + // else + // return moment(str).fromNow(); + + } + + function jumpToEventZms(connkey, dirn) + { + + if ($scope.defaultVideo !== undefined && $scope.defaultVideo != '') + { + + $ionicLoading.show( + { + template: $translate.instant('kEventNavVidFeeds'), + noBackdrop: true, + duration: 3000 + }); + return; + + } + var cmd = dirn == 1 ? '13' : '12'; + $scope.d_eventId = "..."; + NVRDataModel.debug("Sending " + cmd + " to " + connkey); + + $ionicLoading.show( + { + template: $translate.instant('kSwitchingEvents') + "...", + noBackdrop: true, + duration: zm.httpTimeout + }); + + //console.log("Send command connkey: " + connkey); + sendCommand(cmd, connkey) + .then( + function(success) + { + //console.log ("jump success " + JSON.stringify(success)); + $ionicLoading.hide(); + }, + function(error) + { + + NVRDataModel.debug("Hmm jump error " + JSON.stringify(error)); + NVRDataModel.stopNetwork("EventModalCtrl-jumptoEventZms error"); + $scope.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + // console.log ("********* OFFSET FROM JUMPTOEVENTZMS ERROR"); + $timeout(function() + { + sendCommand('14', $scope.connKey, '&offset=' + $scope.currentProgress.progress); + }, 500); + NVRDataModel.debug("so I'm regenerating Connkey to " + $scope.connKey); + //$timeout.cancel(eventQueryHandle); + // eventQueryHandle = $timeout (function(){checkEvent();}, zm.eventPlaybackQuery); + $ionicLoading.hide(); + }); + var slidein; + var slideout; + if (dirn == 1) + { + slideout = "animated slideOutLeft"; + slidein = "animated slideInRight"; + } + else + { + slideout = "animated slideOutRight"; + slidein = "animated slideInLeft"; + } + var element = angular.element(document.getElementById("full-screen-event")); + element.addClass(slideout).one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', outWithOld); + + function outWithOld() + { + + $timeout(function() + { + element.removeClass(slideout); + element.addClass(slidein) + .one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', inWithNew); + + }, 200); + } + + function inWithNew() + { + element.removeClass(slidein); + + } + + } + + //-------------------------------------------------------- + // utility function + //-------------------------------------------------------- + + function computeRelativePath(event) + { + var relativePath = ""; + var loginData = NVRDataModel.getLogin(); + var str = event.Event.StartTime; + var yy = moment(str).locale('en').format('YY'); + var mm = moment(str).locale('en').format('MM'); + var dd = moment(str).locale('en').format('DD'); + var hh = moment(str).locale('en').format('HH'); + var min = moment(str).locale('en').format('mm'); + var sec = moment(str).locale('en').format('ss'); + relativePath = event.Event.MonitorId + "/" + + yy + "/" + + mm + "/" + + dd + "/" + + hh + "/" + + min + "/" + + sec + "/"; + return relativePath; + + } + + //-------------------------------------------------------- + // utility function + //-------------------------------------------------------- + + function computeBasePath(event) + { + var basePath = ""; + var loginData = NVRDataModel.getLogin(); + var str = event.Event.StartTime; + var yy = moment(str).locale('en').format('YY'); + var mm = moment(str).locale('en').format('MM'); + var dd = moment(str).locale('en').format('DD'); + var hh = moment(str).locale('en').format('HH'); + var min = moment(str).locale('en').format('mm'); + var sec = moment(str).locale('en').format('ss'); + + basePath = loginData.url + "/events/" + + event.Event.MonitorId + "/" + + yy + "/" + + mm + "/" + + dd + "/" + + hh + "/" + + min + "/" + + sec + "/"; + return basePath; + } + + //------------------------------------------------------------------------- + // Called when rncarousel or video player finished playing event + //------------------------------------------------------------------------- + + $scope.playbackFinished = function() + { + playbackFinished(); + }; + + function playbackFinished() + { + // currentEvent is updated with the currently playing event in prepareModalEvent() + NVRDataModel.log("Playback of event " + currentEvent.Event.Id + " is finished"); + + if ($scope.loginData.gapless) + { + + neighborEvents(currentEvent.Event.Id) + .then(function(success) + { + + // lets give a second before gapless transition to the next event + $timeout(function() + { + $scope.nextId = success.next; + $scope.prevId = success.prev; + NVRDataModel.debug("Gapless move to event " + $scope.nextId); + jumpToEvent($scope.nextId, 1); + }, 1000); + }, + function(error) + { + NVRDataModel.debug("Error in neighbor call " + + JSON.stringify(error)); + }); + } + else + { + NVRDataModel.debug("not going to next event, gapless is off"); + } + } + + //-------------------------------------------------------- + // Called by openModal as well as jump to event + // what it basically does is get a detailed event API + // for an event ID and constructs required playback + // parameters + // Note that openModal is called with the top level event + // API. Some parameters are repeated across both + //-------------------------------------------------------- + + function prepareModalEvent(eid) + { + + // Lets get the detailed event API + var loginData = NVRDataModel.getLogin(); + var myurl = loginData.apiurl + '/events/' + eid + ".json"; + NVRDataModel.log("*** Constructed API for detailed events: " + myurl); + $scope.humanizeTime = "..."; + $scope.mName = "..."; + $http.get(myurl) + .then(function(success) + { + + // console.log ("DUCCESS::"+JSON.stringify(success)); + var event = success.data.event; + currentEvent = event; + + event.Event.BasePath = computeBasePath(event); + event.Event.relativePath = computeRelativePath(event); + + event.Event.streamingURL = NVRDataModel.getStreamingURL(event.Event.MonitorId); + // event.Event.baseURL = NVRDataModel.getBaseURL (event.Event.MonitorId); + event.Event.baseURL = loginData.url; + event.Event.imageMode = NVRDataModel.getImageMode(event.Event.MonitorId); + + //console.log (JSON.stringify( success)); + $scope.eventName = event.Event.Name; + $scope.eventId = event.Event.Id; + $scope.d_eventId = $scope.eventId; + $scope.eFramesNum = event.Event.Frames; + $scope.eventDur = Math.round(event.Event.Length); + $scope.loginData = NVRDataModel.getLogin(); + $scope.humanizeTime = humanizeTime(event.Event.StartTime); + $scope.mName = NVRDataModel.getMonitorName(event.Event.MonitorId); + //console.log (">>>>>>>>HUMANIZE " + $scope.humanizeTime); + + //console.log("**** VIDEO STATE IS " + event.Event.DefaultVideo); + if (typeof event.Event.DefaultVideo === 'undefined') + event.Event.DefaultVideo = ""; + + $scope.defaultVideo = event.Event.DefaultVideo; + + //console.log("loginData is " + JSON.stringify($scope.loginData)); + //console.log("Event ID is " + $scope.eventId); + //console.log("video is " + $scope.defaultVideo); + + neighborEvents(event.Event.Id) + .then(function(success) + { + $scope.nextId = success.next; + $scope.prevId = success.prev; + }, + function(error) + { + //console.log(JSON.stringify(error)); + }); + + $scope.nextId = "..."; + $scope.prevId = "..."; + + event.Event.video = {}; + var videoURL; + + if ((event.Event.imageMode == 'path') || NVRDataModel.getLogin().forceImageModePath) + videoURL = event.Event.baseURL + "/events/" + event.Event.relativePath + event.Event.DefaultVideo; + else + videoURL = event.Event.baseURL + "/index.php?view=view_video&eid=" + event.Event.Id; + + // hack + //videoURL = "http://static.videogular.com/assets/videos/videogular.mp4"; + //videoURL = "http://arjunrc.ddns.net:8888/foo2.mp4"; + $scope.video_url = videoURL; + + console.log("************** VIDEO IS " + videoURL); + + NVRDataModel.debug("Video url passed to player is: " + videoURL); + + // console.log (">>>>>>>>>>>>>"+loginData.url+"-VS-"+event.Event.baseURL); + + //console.log("************** VIDEO IS " + videoURL); + + $scope.videoObject = { + config: + { + autoPlay: true, + responsive: false, + nativeControls: false, + nativeFullScreen:false, + + playsInline: true, + sources: [ + { + src: $sce.trustAsResourceUrl(videoURL), + type: "video/mp4" + } + + ], + + theme: "lib/videogular-themes-default/videogular.css", + cuepoints: { + theme: { + url:"lib/videogular-cuepoints/cuepoints.css" + }, + points: [], + } + } + }; + + // $scope.videoObject = angular.copy(event.Event.video); + + $scope.playbackURL = $scope.loginData.url; + + $scope.videoIsReady = true; + + /* we don't need this for electron + if ($rootScope.platformOS == "desktop") { + $scope.playbackURL = zm.desktopUrl; + } */ + + $scope.eventBasePath = event.Event.BasePath; + $scope.relativePath = event.Event.relativePath; + $rootScope.rand = Math.floor(Math.random() * (999999 - 111111 + 1)) + 111111; + + $scope.slider_modal_options = { + from: 1, + to: event.Event.Frames, + realtime: true, + step: 1, + className: "mySliderClass", + callback: function(value, released) + { + //console.log("CALLBACK"+value+released); + $ionicScrollDelegate.freezeScroll(!released); + + }, + //modelLabels:function(val) {return "";}, + smooth: false, + css: + { + background: + { + "background-color": "silver" + }, + before: + { + "background-color": "purple" + }, + default: + { + "background-color": "white" + }, // default value: 1px + after: + { + "background-color": "green" + }, // zone after default value + pointer: + { + "background-color": "red" + }, // circle pointer + range: + { + "background-color": "red" + } // use it if double value + }, + scale: [] + + }; + + $scope.mycarousel.index = 0; + $scope.ionRange.index = 1; + $scope.eventSpeed = $scope.event.Event.Length / $scope.event.Event.Frames; + + //console.log("**Resetting range"); + $scope.slides = []; + var i; + for (i = 1; i <= event.Event.Frames; i++) + { + var fname = padToN(i, eventImageDigits) + "-capture.jpg"; + // console.log ("Building " + fname); + $scope.slides.push( + { + id: i, + img: fname + }); + } + + // now get event details to show alarm frames + + //$scope.FrameArray = event.Frame; + // $scope.slider_options.scale=[]; + // $scope.slider_modal_options.scale = []; + + // lets + framearray.datasets[0].data = []; + for (i = 0; i < event.Frame.length; i++) + { + + var ts = moment(event.Frame[i].TimeStamp).format(timeFormat); + + //console.log ("pushing s:" + event.Frame[i].Score+" t:"+ts); + + framearray.datasets[0].data.push( + { + x: ts, + y: event.Frame[i].Score + }); + framearray.labels.push(""); + + } + $scope.totalEventTime = Math.round(parseFloat(event.Event.Length)) - 1; + $scope.currentEventTime = 0; + + // video mode doesn't need this graph - it won't really work + if ($scope.defaultVideo == undefined || $scope.defaultVideo == '') + { + $timeout(function() + { + drawGraph(); + }, 500); + } + + }, + function(err) + { + NVRDataModel.log("Error retrieving detailed frame API " + JSON.stringify(err)); + NVRDataModel.displayBanner('error', ['could not retrieve frame details', 'please try again']); + }); + + } + + if (typeof $scope.ionRange !== 'undefined') + { + $scope.$watch('ionRange.index', function() + { + // + $scope.mycarousel.index = parseInt($scope.ionRange.index) - 1; + + if (carouselUtils.getStop() == true) + return; + + //console.log ("***ION RANGE CHANGED TO " + $scope.mycarousel.index); + }); + } + + if (typeof $scope.mycarousel !== 'undefined') + { + $scope.$watch('mycarousel.index', function() + { + + if (currentEvent && $scope.ionRange.index == parseInt(currentEvent.Event.Frames - 1)) + { + playbackFinished(); + } + // end of playback from quick scrub + // so ignore gapless + + if ($scope.event && $scope.ionRange.index == parseInt($scope.event.Event.Frames) - 1) + { + if (!$scope.modal || $scope.modal.isShown() == false) + { + // console.log("quick scrub playback over"); + carouselUtils.setStop(true); + $scope.ionRange.index = 0; + $scope.mycarousel.index = 1; + } + + } + if (carouselUtils.getStop() == true) + return; + $scope.ionRange.index = ($scope.mycarousel.index + 1).toString(); + // console.log ("***IONRANGE RANGE CHANGED TO " + $scope.ionRange.index); + + }); + } + + function padToN(number, digits) + { + + var i; + var stringMax = ""; + var stringLeading = ""; + for (i = 1; i <= digits; i++) + { + stringMax = stringMax + "9"; + if (i != digits) stringLeading = stringLeading + "0"; + } + var numMax = parseInt(stringMax); + + if (number <= numMax) + { + number = (stringLeading + number).slice(-digits); + } + //console.log ("PADTON: returning " + number); + return number; + } + +}]); diff --git a/www/js/EventServer.js b/www/js/EventServer.js new file mode 100644 index 00000000..b0d0c83f --- /dev/null +++ b/www/js/EventServer.js @@ -0,0 +1,584 @@ +/* jshint -W041 */ + +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console ,PushNotification*/ + +//-------------------------------------------------------------------------- +// This factory interacts with the ZM Event Server +// over websockets and is responsible for rendering real time notifications +//-------------------------------------------------------------------------- + +angular.module('zmApp.controllers') + .factory('EventServer', ['NVRDataModel', '$rootScope', '$websocket', '$ionicPopup', '$timeout', '$q', 'zm', '$ionicPlatform', '$cordovaMedia', '$translate', function(NVRDataModel, $rootScope, $websocket, $ionicPopup, $timeout, $q, zm, $ionicPlatform, $cordovaMedia, $translate) + { + + var lastEventServerCheck = Date.now(); + var ws; + + var localNotificationId = 0; + var firstError = true; + + //-------------------------------------------------------------------------- + // called when the websocket is opened + //-------------------------------------------------------------------------- + function openHandshake() + { + var loginData = NVRDataModel.getLogin(); + if (loginData.isUseEventServer == false || loginData.eventServer == "") + { + NVRDataModel.log("openHandShake: no event server"); + return; + } + + NVRDataModel.log("openHandshake: Websocket open"); + ws.$emit('auth', + { + user: loginData.username, + password: loginData.password + }); + + if ($rootScope.apnsToken != '') + { + var plat = $ionicPlatform.is('ios') ? 'ios' : 'android'; + var ld = NVRDataModel.getLogin(); + var pushstate = "enabled"; + if (ld.disablePush == true) + pushstate = "disabled"; + + NVRDataModel.debug("openHandShake: state of push is " + pushstate); + // let's do this only if disabled. If enabled, I suppose registration + // will be called? + //if (ld.disablePush) + if (1) + { + //console.log ("HANDSHAKE MESSAGE WITH "+$rootScope.monstring); + ws.$emit('push', + { + type: 'token', + platform: plat, + token: $rootScope.apnsToken, + monlist:$rootScope.monstring, + intlist:$rootScope.intstring, + state: pushstate + }); + } + } + + } + + //-------------------------------------------------------------------------- + // Called once at app start. Does a lazy definition of websockets open + //-------------------------------------------------------------------------- + function init() + { + + $rootScope.isAlarm = 0; + $rootScope.alarmCount = "0"; + + var d = $q.defer(); + + var loginData = NVRDataModel.getLogin(); + + //console.log ("INIT GOT " + JSON.stringify(loginData)); + + if (loginData.isUseEventServer == false || !loginData.eventServer) + { + NVRDataModel.log("No Event Server present. Not initializing"); + d.reject("false"); + return d.promise; + } + + //if (!$rootScope.apnsToken) + pushInit(); + + if (typeof ws !== 'undefined') + { + NVRDataModel.debug("Event server already initialized"); + d.resolve("true"); + return d.promise; + } + + NVRDataModel.log("Initializing Websocket with URL " + + loginData.eventServer + " , will connect later..."); + ws = $websocket.$new( + { + url: loginData.eventServer, + reconnect: true, + reconnectInterval: 60000, + lazy: true + }); + + // Transmit auth information to server + ws.$on('$open', openHandshake); + + NVRDataModel.debug("Setting up websocket error handler"); + ws.$on('$error', function(e) + { + + // we don't need this check as I changed reconnect interval to 60s + //if ((Date.now() - lastEventServerCheck > 30000.0) || firstError) + if (1) + { + NVRDataModel.debug("Websocket Errorhandler called"); + $timeout(function() + { + NVRDataModel.displayBanner('error', ['Event Server connection error']); + }, 3000); // leave 3 seconds for transitions + firstError = false; + lastEventServerCheck = Date.now(); + } + //console.log ("VALUE TIME " + lastEventServerCheck); + //console.log ("NOW TIME " + Date.now()); + }); + + ws.$on('$close', function() + { + NVRDataModel.log("Websocket closed"); + + }); + + // Handles responses back from ZM ES + + ws.$on('$message', function(str) + { + NVRDataModel.log("Real-time event: " + JSON.stringify(str)); + + // Error messages + if (str.status != 'Success') + { + NVRDataModel.log("Event Error: " + JSON.stringify(str)); + + if (str.reason == 'APNSDISABLED') + { + ws.$close(); + NVRDataModel.displayBanner('error', ['Event Server: APNS disabled'], 2000, 6000); + $rootScope.apnsToken = ""; + } + + } + + if (str.status == 'Success' && (str.event == 'auth')) + { + if (str.version == undefined) + str.version = "0.1"; + if (NVRDataModel.versionCompare(str.version, zm.minEventServerVersion) == -1) + { + $rootScope.zmPopup = $ionicPopup.alert( + { + title: $translate.instant('kEventServerVersionTitle'), + template: $translate.instant('kEventServerVersionBody1') + " " + str.version + ". " + $translate.instant('kEventServerVersionBody2') + + zm.minEventServerVersion, + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + } + + } + + if (str.status == 'Success' && str.event == 'alarm') // new events + { + + var localNotText; + // ZMN specific hack for Event Server + if (str.supplementary != 'true') + { + new Audio('sounds/blop.mp3').play(); + localNotText = "Latest Alarms: "; + $rootScope.isAlarm = 1; + + // Show upto a max of 99 when it comes to display + // so aesthetics are maintained + if ($rootScope.alarmCount == "99") + { + $rootScope.alarmCount = "99+"; + } + if ($rootScope.alarmCount != "99+") + { + $rootScope.alarmCount = (parseInt($rootScope.alarmCount) + 1).toString(); + } + + } + else + { + NVRDataModel.debug("received supplementary event information over websockets"); + } + var eventsToDisplay = []; + var listOfMonitors = []; + for (var iter = 0; iter < str.events.length; iter++) + { + // lets stack the display so they don't overwrite + eventsToDisplay.push(str.events[iter].Name + ": latest new alarm (" + str.events[iter].EventId + ")"); + localNotText = localNotText + str.events[iter].Name + ","; + listOfMonitors.push(str.events[iter].MonitorId); + + } + localNotText = localNotText.substring(0, localNotText.length - 1); + + // if we are in background, do a local notification, else do an in app display + if (!NVRDataModel.isBackground()) + { + + //emit alarm details - this is when received over websockets + $rootScope.$emit('alarm', + { + message: listOfMonitors + }); + + if (str.supplementary != 'true') + { + + NVRDataModel.debug("App is in foreground, displaying banner"); + if (eventsToDisplay.length > 0) + { + + if (eventsToDisplay.length == 1) + { + //console.log("Single Display: " + eventsToDisplay[0]); + NVRDataModel.displayBanner('alarm', [eventsToDisplay[0]], 5000, 5000); + } + else + { + NVRDataModel.displayBanner('alarm', eventsToDisplay, + 5000, 5000 * eventsToDisplay.length); + } + + } + } + } + + } //end of success handler + + }); + d.resolve("true"); + return (d.promise); + + } + + function disconnect() + { + NVRDataModel.log("Disconnecting and deleting Event Server socket..."); + + if (typeof ws === 'undefined') + return; + + ws.$close(); + ws.$un('open'); + ws.$un('close'); + ws.$un('message'); + ws = undefined; + + } + + //-------------------------------------------------------------------------- + // Send an arbitrary object to the Event Serve + // currently planned to use it for device token + // isForce =1 when you need to send the message even + // if config says ES is off. This may happen when + // you turn off ES and then we need sendMessage to + // let ZMES know not to send us messages + //-------------------------------------------------------------------------- + function sendMessage(type, obj, isForce) + { + var ld = NVRDataModel.getLogin(); + if (ld.isUseEventServer == false && isForce != 1) + { + NVRDataModel.debug("Not sending WSS message as event server is off"); + return; + } + + if (typeof ws === 'undefined') + { + NVRDataModel.debug("Event server not initalized, not sending message"); + return; + } + + if (ws.$status() == ws.$CLOSED) + { + NVRDataModel.log("Websocket was closed, trying to re-open"); + ws.$un('$open'); + //ws.$on ('$open', openHandshake); + ws.$open(); + + ws.$on('$open', openHandshake, function() + { + + //console.log(" sending " + type + " " + + // JSON.stringify(obj)); + //console.log("sending " + type + " " + JSON.stringify(obj)); + ws.$emit(type, obj); + + ws.$un('$open'); + ws.$on('$open', openHandshake); + + }); + + } + else + { + ws.$emit(type, obj); + // console.log("sending " + type + " " + JSON.stringify(obj)); + } + + } + + //-------------------------------------------------------------------------- + // Called each time we resume + //-------------------------------------------------------------------------- + function refresh() + { + var loginData = NVRDataModel.getLogin(); + + if ((!loginData.eventServer) || (loginData.isUseEventServer == false)) + { + NVRDataModel.log("No Event Server configured, skipping refresh"); + + // Let's also make sure that if the socket was open + // we close it - this may happen if you disable it after using it + + if (typeof ws !== 'undefined') + { + if (ws.$status() != ws.$CLOSED) + { + NVRDataModel.debug("Closing open websocket as event server was disabled"); + ws.$close(); + } + } + + return; + } + + if (typeof ws === 'undefined') + { + NVRDataModel.debug("Calling websocket init"); + init(); + } + + // refresh is called when + // The following situations will close the socket + // a) In iOS the client went to background -- we should reconnect + // b) The Event Server died + // c) The network died + // Seems to me in all cases we should give re-open a shot + + if (ws.$status() == ws.$CLOSED) + { + NVRDataModel.log("Websocket was closed, trying to re-open"); + ws.$open(); + } + + } + + function pushInit() + { + NVRDataModel.log(">>>Setting up push registration"); + var push; + var mediasrc; + var media; + var ld = NVRDataModel.getLogin(); + + var plat = $ionicPlatform.is('ios') ? 'ios' : 'android'; + + if ($rootScope.platformOS == 'desktop') + { + NVRDataModel.log("Desktop instance, not setting up push. Websockets only, I hope"); + return; + } + + if (plat == 'ios') + { + mediasrc = "sounds/blop.mp3"; + push = PushNotification.init( + + { + "ios": + { + "alert": true, + "badge": true, + "sound": ld.soundOnPush, + "clearBadge": true + } + } + + ); + + } + else + { + mediasrc = "/android_asset/www/sounds/blop.mp3"; + var android_media_file = "blop"; + + push = PushNotification.init( + + { + "android": + { + "senderID": zm.gcmSenderId, + "icon": "ic_stat_notification", + sound: ld.soundOnPush, + vibrate: ld.vibrateOnPush + //"sound": android_media_file + } + } + + ); + + } + + // console.log("*********** MEDIA BLOG IS " + mediasrc); + media = $cordovaMedia.newMedia(mediasrc); + + push.on('registration', function(data) + { + NVRDataModel.debug("Push Notification registration ID received: " + JSON.stringify(data)); + $rootScope.apnsToken = data.registrationId; + + var plat = $ionicPlatform.is('ios') ? 'ios' : 'android'; + var ld = NVRDataModel.getLogin(); + var pushstate = "enabled"; + if (ld.disablePush == true) + pushstate = "disabled"; + + // now at this stage, if this is a first registration + // zmeventserver will have no record of this token + // so we need to make sure we send it a legit list of + // monitors otherwise users will get notifications for monitors + // their login is not supposed to see. Refer #391 + + var monstring=''; + var intstring=''; + NVRDataModel.getMonitors() + .then (function(succ) { + var mon = succ; + for (var i = 0; i < mon.length; i++) { + monstring = monstring + mon[i].Monitor.Id + ","; + intstring = intstring + '0,'; + } + if (monstring.charAt(monstring.length - 1) == ',') + monstring = monstring.substr(0, monstring.length - 1); + + if (intstring.charAt(intstring.length - 1) == ',') + intstring = intstring.substr(0, intstring.length - 1); + + //console.log ("WUTPUT SENDING REG WITH "+monstring); + + $rootScope.monstring = monstring; + $rootScope.intstring = intstring; + + sendMessage('push', + { + type: 'token', + platform: plat, + token: $rootScope.apnsToken, + monlist: monstring, + intlist: intstring, + state: pushstate + }, 1); + + }, + function (err) + { + NVRDataModel.log ("Could not get monitors, can't send push reg"); + }); + + }); + + push.on('notification', function(data) + { + + NVRDataModel.debug("received push notification"); + + var ld = NVRDataModel.getLogin(); + if (ld.isUseEventServer == false) + { + NVRDataModel.debug("received push notification, but event server disabled. Not acting on it"); + return; + } + + if (data.additionalData.foreground == false) + { + // This means push notification tap in background + + NVRDataModel.debug("*** PUSH NOTFN.>>>>" + JSON.stringify(data)); + + // set tappedMid to monitor + //*** PUSH DATA>>>>{"sound":"blop","message":"Alarms: Basement (2854) ","additionalData":{"mid":"2","coldstart":false,"collapse_key":"do_not_collapse","foreground":false}} + + NVRDataModel.debug("Notification Tapped"); + $rootScope.alarmCount = "0"; + $rootScope.isAlarm = 0; + $rootScope.tappedNotification = 1; + var mid = data.additionalData.mid; + + // if Multiple mids, take the first one + var mi = mid.indexOf(','); + if (mi > 0) + { + mid = mid.slice(0, mi); + } + mid = parseInt(mid); + + $rootScope.tappedMid = mid; + NVRDataModel.log("Push notification: Tapped Monitor taken as:" + $rootScope.tappedMid); + + if ($rootScope.platformOS == 'ios') + { + + NVRDataModel.debug("iOS only: clearing background push"); + push.finish(function() + { + NVRDataModel.debug("processing of push data is finished"); + }); + } + + } + else + { + + // this flag honors the HW mute button. Go figure + // http://ilee.co.uk/phonegap-plays-sound-on-mute/ + if (ld.soundOnPush) + { + media.play( + { + playAudioWhenScreenIsLocked: false + }); + } + + var str = data.message; + // console.log ("***STRING: " + str + " " +str.status); + var eventsToDisplay = []; + + NVRDataModel.displayBanner('alarm', [str], 0, 5000 * eventsToDisplay.length); + + $rootScope.isAlarm = 1; + + // Show upto a max of 99 when it comes to display + // so aesthetics are maintained + if ($rootScope.alarmCount == "99") + { + $rootScope.alarmCount = "99+"; + } + if ($rootScope.alarmCount != "99+") + { + $rootScope.alarmCount = (parseInt($rootScope.alarmCount) + 1).toString(); + } + } + }); + + push.on('error', function(e) + { + NVRDataModel.debug("Push error: " + JSON.stringify(e)); + // console.log("************* PUSH ERROR ******************"); + }); + } + + return { + refresh: refresh, + init: init, + sendMessage: sendMessage, + pushInit: pushInit, + disconnect: disconnect + + }; + + }]); diff --git a/www/js/EventServerSettingsCtrl.js b/www/js/EventServerSettingsCtrl.js new file mode 100644 index 00000000..efa868ea --- /dev/null +++ b/www/js/EventServerSettingsCtrl.js @@ -0,0 +1,360 @@ + /* jshint -W041 */ + /* jslint browser: true*/ + /* global cordova,StatusBar,angular,console */ + + angular.module('zmApp.controllers').controller('zmApp.EventServerSettingsCtrl', ['$scope', '$ionicSideMenuDelegate', 'zm', '$stateParams', 'EventServer', '$ionicHistory', '$rootScope', '$state', 'message', 'NVRDataModel', '$ionicPlatform', '$ionicPopup', '$timeout', '$translate', function($scope, $ionicSideMenuDelegate, zm, $stateParams, EventServer, $ionicHistory, $rootScope, $state, message, NVRDataModel, $ionicPlatform, $ionicPopup, $timeout, $translate) + { + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + // we need this to dynamically get title + // name as ion-view is set in stone and + // we don't get title till beforeEnter + // which is odd - I'd expect beforeEnter to load + // before View is loaded + $scope.getTitle = function() + { + return $scope.loginData.serverName; + }; + + //---------------------------------------------------------------- + // Save anyway when you exit + //---------------------------------------------------------------- + + $scope.$on('$ionicView.beforeLeave', function() + { + saveItems(); + + }); + + $scope.$on('$ionicView.beforeEnter', function() + { + + $scope.loginData = NVRDataModel.getLogin(); + //console.log ("Event server - before Enter, loginData is " + JSON.stringify($scope.loginData)); + $scope.defScreen = $scope.loginData.onTapScreen; + + if ($scope.loginData.eventServer == "") + { + $scope.loginData.eventServer = "wss://" + extractDomain($scope.loginData.url) + ":9000"; + } + + res = $scope.loginData.eventServerMonitors.split(","); + minterval = $scope.loginData.eventServerInterval.split(","); + + var monchecked = false; + for (var i = 0; i < $scope.monitors.length; i++) + { + + if (!isEnabled($scope.monitors[i].Monitor.Id)) + { + // if the filter list has IDs and this is not part of it, uncheck it + $scope.monitors[i].Monitor.isChecked = false; + //console.log("Marking false"); + $scope.monitors[i].Monitor.reportingInterval = 0; + } + else + { + // console.log("Marking true"); + $scope.monitors[i].Monitor.isChecked = true; + $scope.monitors[i].Monitor.reportingInterval = getInterval($scope.monitors[i].Monitor.Id); + monchecked = true; + } + + } + + // now if none are checked, assume it means all checked. This is related to the + // fact that ES will start sending all monitors, even ones you don't have access to + if (!monchecked) + { + NVRDataModel.debug ("Enabling all monitors for event server"); + for (var j = 0; j < $scope.monitors.length; j++) + { + $scope.monitors[i].Monitor.isChecked = true; + $scope.monitors[i].Monitor.reportingInterval = 0; + } + + } + }); + + //-------------------------------------------------- + // notification tap action + //-------------------------------------------------- + + $scope.selectScreen = function() + { + + var ld = NVRDataModel.getLogin(); + + $scope.myopt = { + selectedState: ld.onTapScreen + }; + + var options = '<ion-radio-fix ng-model="myopt.selectedState" ng-value="\'' + $translate.instant('kTapEvents') + '\'">' + $translate.instant('kTapEvents') + '</ion-radio-fix>'; + + options += '<ion-radio-fix ng-model="myopt.selectedState" ng-value="\'' + $translate.instant('kTapMontage') + '\'">' + $translate.instant('kTapMontage') + '</ion-radio-fix>'; + options += '<ion-radio-fix ng-model="myopt.selectedState" ng-value="\'' + $translate.instant('kTapLiveMonitor') + '\'">' + $translate.instant('kTapLiveMonitor') + '</ion-radio-fix>'; + + $rootScope.zmPopup = $ionicPopup.show( + { + scope: $scope, + template: options, + + title: 'View to navigate to:', + subTitle: 'currently set to: ' + ld.onTapScreen, + buttons: [ + { + text: $translate.instant('kButtonCancel'), + + }, + { + text: $translate.instant('kButtonOk'), + onTap: function(e) + { + + ld.onTapScreen = $scope.myopt.selectedState; + NVRDataModel.log("Setting new onTap State:" + ld.onTapScreen); + NVRDataModel.setLogin(ld); + $scope.defScreen = $scope.myopt.selectedState; + $scope.loginData = ld; + + } + }] + }); + + }; + + //---------------------------------------------------------------- + // Accordion list show/hide + //---------------------------------------------------------------- + + $scope.toggleGroup = function(group) + { + if ($scope.isGroupShown(group)) + { + $scope.shownGroup = null; + } + else + { + $scope.shownGroup = group; + } + }; + $scope.isGroupShown = function(group) + { + return $scope.shownGroup === group; + }; + + $scope.saveItems = function() + { + saveItems(); + }; + + //---------------------------------------------------------------- + // Saves ES data + //---------------------------------------------------------------- + + function saveItems() + { + NVRDataModel.debug("Saving Event Server data"); + var monstring = ""; + var intervalstring = ""; + var plat = $ionicPlatform.is('ios') ? 'ios' : 'android'; + for (var i = 0; i < $scope.monitors.length; i++) + { + if (isNaN($scope.monitors[i].Monitor.reportingInterval)) + { + $scope.monitors[i].Monitor.reportingInterval = 0; + } + if ($scope.monitors[i].Monitor.isChecked) + { + monstring = monstring + $scope.monitors[i].Monitor.Id + ","; + var tint = parseInt($scope.monitors[i].Monitor.reportingInterval); + if (isNaN(tint)) tint = 0; + intervalstring = intervalstring + tint + ","; + } + + } + + if (monstring.charAt(monstring.length - 1) == ',') + monstring = monstring.substr(0, monstring.length - 1); + + if (intervalstring.charAt(intervalstring.length - 1) == ',') + intervalstring = intervalstring.substr(0, intervalstring.length - 1); + + $scope.loginData.eventServerMonitors = monstring; + $scope.loginData.eventServerInterval = intervalstring; + + //console.log ("SAVED: " + JSON.stringify($scope.loginData)); + NVRDataModel.setLogin($scope.loginData); + + var pushstate = "enabled"; + if ($scope.loginData.disablePush == true || $scope.loginData.isUseEventServer == false) + pushstate = "disabled"; + + if ($scope.loginData.isUseEventServer == true) + { + EventServer.init() + .then(function(data) + { + // console.log("Sending control filter"); + NVRDataModel.debug("Sending Control message 'filter' with monlist=" + monstring + " and interval=" + intervalstring); + EventServer.sendMessage("control", + { + type: 'filter', + monlist: monstring, + intlist: intervalstring + }, 1); + + if ($rootScope.apnsToken != "") + // if its defined then this is post init work + // so lets transmit state here + + { + // we need to disable the token + NVRDataModel.debug("Sending token state " + pushstate); + EventServer.sendMessage('push', + { + type: 'token', + platform: plat, + token: $rootScope.apnsToken, + state: pushstate + }, 1); + + } + + }); + + } + else + { + if ($rootScope.apnsToken != "") + // if its defined then this is post init work + // so lets transmit state here + + { + // we need to disable the token + NVRDataModel.debug("Sending token state " + pushstate); + EventServer.sendMessage('push', + { + type: 'token', + platform: plat, + token: $rootScope.apnsToken, + state: pushstate + }, 1); + + } + // Give the above some time to transmit + + EventServer.disconnect(); + + } + + NVRDataModel.displayBanner('info', ['settings saved']); + } + + //---------------------------------------------------------------- + // returns domain name in string - + // http://stackoverflow.com/questions/8498592/extract-root-domain-name-from-string + //---------------------------------------------------------------- + function extractDomain(url) + { + var domain; + //find & remove protocol (http, ftp, etc.) and get domain + if (url.indexOf("://") > -1) + { + domain = url.split('/')[2]; + } + else + { + domain = url.split('/')[0]; + } + + //find & remove port number + domain = domain.split(':')[0]; + + return domain; + } + + //---------------------------------------------------------------- + // returns reporting interval for monitor ID + //---------------------------------------------------------------- + function getInterval(id) + { + // means no interval, should only happen one time + // till we save + if ($scope.loginData.eventServerInterval == "") + return 0; + var retval = 0; + for (var i = 0; i < res.length; i++) + { + if (res[i] == id) + { + retval = parseInt(minterval[i]); + break; + } + } + return retval; + } + + //---------------------------------------------------------------- + // Returns true/false if monitor ID is in event monitor list + //---------------------------------------------------------------- + function isEnabled(id) + { + if ($scope.loginData.eventServerMonitors == "") + return true; + + var isThere = false; + for (var i = 0; i < res.length; i++) + { + if (res[i] == id) + { + isThere = true; + //console.log("isRes found: " + id); + break; + } + } + return isThere; + } + + //------------------------------------------------------------------------- + // Controller Main + //------------------------------------------------------------------------ + $scope.monitors = []; + $scope.monitors = message; + var res, minterval; + + }]); diff --git a/www/js/EventsGraphsCtrl.js b/www/js/EventsGraphsCtrl.js new file mode 100644 index 00000000..5f21b09b --- /dev/null +++ b/www/js/EventsGraphsCtrl.js @@ -0,0 +1,250 @@ +/* jshint -W041 */ +/* jshint -W083 */ +/*This is for the loop closure I am using in line 143 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console,moment */ + +// This controller generates a graph for events +// the main function is generateChart. I call generate chart with required parameters +// from the template file + +angular.module('zmApp.controllers').controller('zmApp.EventsGraphsCtrl', ['$ionicPlatform', '$scope', 'zm', 'NVRDataModel', '$ionicSideMenuDelegate', '$rootScope', '$http', '$ionicHistory', '$state', function($ionicPlatform, $scope, zm, NVRDataModel, $ionicSideMenuDelegate, $rootScope, $http, $ionicHistory, $state) +{ + //console.log("Inside Graphs controller"); + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + $scope.$on('$ionicView.loaded', function() + { + // console.log("**VIEW ** Graph Ctrl Loaded"); + }); + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + //------------------------------------------------------------------------- + // Lets make sure we set screen dim properly as we enter + // The problem is we enter other states before we leave previous states + // from a callback perspective in ionic, so we really can't predictably + // reset power state on exit as if it is called after we enter another + // state, that effectively overwrites current view power management needs + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + // console.log("**VIEW ** EventsGraphs Ctrl Entered"); + NVRDataModel.setAwake(false); + }); + + $scope.$on('$ionicView.leave', function() + { + // console.log("**VIEW ** Graph Ctrl Left"); + }); + + $scope.$on('$ionicView.unloaded', function() + { + // console.log("**VIEW ** Graph Ctrl Unloaded"); + }); + + //------------------------------------------------- + // Controller main + //------------------------------------------------- + + // $scope.chart = ""; + $scope.navTitle = 'Tab Page'; + // $scope.chart=""; + $scope.leftButtons = [ + { + type: 'button-icon icon ion-navicon', + tap: function(e) + { + $scope.toggleMenu(); + } + }]; + + var container = angular.element(document.getElementById('visualization')); + //console.log(JSON.stringify(container)); + var data = [ + { + id: 1, + content: 'item 1', + start: '2013-04-20' + }, + { + id: 2, + content: 'item 2', + start: '2013-04-14' + }, + { + id: 3, + content: 'item 3', + start: '2013-04-18' + }, + { + id: 4, + content: 'item 4', + start: '2013-04-16', + end: '2013-04-19' + }, + { + id: 5, + content: 'item 5', + start: '2013-04-25' + }, + { + id: 6, + content: 'item 6', + start: '2013-04-27' + }]; + var options = {}; + //var timeline = new vis.Timeline(container[0], data, options); + + // ------------------------------------------------- + // Called when user taps on a bar + //--------------------------------------------------- + $scope.handleChartClick = function(event) + { + + //console.log(JSON.stringify($scope.chartwithbars.getBarsAtEvent(event))); + //console.log(angular.element[0].getContext('2d')); + //console.log (JSON.stringify( $scope.chart)); + + }; + + //------------------------------------------------- + // Generates a bar graph with data provided + //------------------------------------------------- + $scope.generateTCChart = function(id, chartTitle, hrs) + { + var monitors = []; + var dateRange = ""; + var startDate = ""; + var endDate = ""; + + $scope.chart = { + barHeight: "", + data: "", + options: "" + + }; + + $scope.chart.barHeight = $rootScope.devHeight; + + if (hrs) + { + // Apply a time based filter if I am not watching all events + var cur = moment(); + endDate = cur.format("YYYY-MM-DD " + NVRDataModel.getTimeFormat()); + startDate = cur.subtract(hrs, 'hours').format("YYYY-MM-DD " + NVRDataModel.getTimeFormat()); + //console.log("Start and End " + startDate + "==" + endDate); + NVRDataModel.log("Generating graph for " + startDate + " to " + endDate); + + } + + var loginData = NVRDataModel.getLogin(); + //$scope.chart.data = {}; + $scope.chart.data = { + labels: [], + datasets: [ + { + label: '', + fillColor: zm.graphFillColor, + strokeColor: zm.graphStrokeColor, + highlightFill: zm.graphHighlightFill, + data: [] + }, ] + }; + + NVRDataModel.getMonitors(0).then(function(data) + { + monitors = data; + var adjustedHeight = monitors.length * 30; + if (adjustedHeight > $rootScope.devHeight) + { + + $scope.chart.barHeight = adjustedHeight; + //console.log("********* BAR HEIGHT TO " + $scope.chart.barHeight); + } + + for (var i = 0; i < monitors.length; i++) + { + (function(j) + { // loop closure - http is async, so success returns after i goes out of scope + // so we need to bind j to i when http returns so its not out of scope. Gak. + // I much prefer the old days of passing context data from request to response + + $scope.chart.data.labels.push(monitors[j].Monitor.Name); + + //$scope.chartObject[id].data.push([monitors[j].Monitor.Name,'0','color:#76A7FA','0']); + // $scope.chartObject.data[j+1]=([monitors[j].Monitor.Name,'100','color:#76A7FA','0']); + + var dateString = ""; + if (hrs) + { + dateString = "/StartTime >=:" + startDate + "/EndTime <=:" + endDate; + } + var url = loginData.apiurl + + "/events/index/MonitorId:" + monitors[j].Monitor.Id + dateString + + ".json?page=1"; + // console.log("Monitor event URL:" + url); + NVRDataModel.log("EventGraph: composed url is " + url); + $http.get(url /*,{timeout:15000}*/ ) + .success(function(data) + { + NVRDataModel.debug("Event count for monitor" + + monitors[j].Monitor.Id + " is " + data.pagination.count); + $scope.chart.data.datasets[0].data[j] = data.pagination.count; + }) + .error(function(data) + { + // ideally I should be treating it as an error + // but what I am really doing now is treating it like no events + // works but TBD: make this into a proper error handler + $scope.chart.data.datasets[0].data[j] = 0; + NVRDataModel.log("Error retrieving events for graph " + JSON.stringify(data), "error"); + }); + })(i); // j + } //for + }); + + $scope.chart.options = { + + responsive: true, + scaleBeginAtZero: true, + scaleShowGridLines: false, + scaleGridLineColor: "rgba(0,0,0,.05)", + scaleGridLineWidth: 1, + barShowStroke: true, + barStrokeWidth: 2, + barValueSpacing: 5, + barDatasetSpacing: 1, + showTooltip: true, + + //String - A legend template + // legendTemplate : '<ul class="tc-chart-js-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>' + }; + }; //generateTCChart +}]); diff --git a/www/js/EventsModalGraphCtrl.js b/www/js/EventsModalGraphCtrl.js new file mode 100644 index 00000000..fc9a9c42 --- /dev/null +++ b/www/js/EventsModalGraphCtrl.js @@ -0,0 +1,409 @@ +// Common Controller for the montage view +/* jshint -W041 */ +/* jslint browser: true*/ +/* global saveAs, cordova,StatusBar,angular,console,ionic, moment, vis , Chart, DJS*/ + +angular.module('zmApp.controllers').controller('EventsModalGraphCtrl', ['$scope', '$rootScope', 'zm', 'NVRDataModel', '$ionicSideMenuDelegate', '$timeout', '$interval', '$ionicModal', '$ionicLoading', '$http', '$state', '$stateParams', '$ionicHistory', '$ionicScrollDelegate', '$q', '$sce', 'carouselUtils', '$ionicPopup', '$translate', function($scope, $rootScope, zm, NVRDataModel, $ionicSideMenuDelegate, $timeout, $interval, $ionicModal, $ionicLoading, $http, $state, $stateParams, $ionicHistory, $ionicScrollDelegate, $q, $sce, carouselUtils, $ionicPopup, $translate) +{ + + var Graph2d; + var tcGraph; + var items; + var groups; + var eventImageDigits = 5; + var cv; + var ctx; + //var options; + //var data; + var onlyalarm_data; + var current_data; + var current_options; + var btype; + var data, options; + + $scope.$on('modal.shown', function(e, m) + { + + if (m.id != 'modalgraph') + return; + + //console.log ("INSIDE MODAL GRAPH>>>>>>>>>>>>>>>>>"); + data = { + labels: ["January", "February", "March", "April", "May", "June", "July"], + datasets: [ + { + label: "My First dataset", + fillColor: "rgba(220,220,220,0.5)", + strokeColor: "rgba(220,220,220,0.8)", + highlightFill: "rgba(220,220,220,0.75)", + highlightStroke: "rgba(220,220,220,1)", + data: [65, 59, 80, 81, 56, 55, 40] + }, + { + label: "My Second dataset", + fillColor: "rgba(151,187,205,0.5)", + strokeColor: "rgba(151,187,205,0.8)", + highlightFill: "rgba(151,187,205,0.75)", + highlightStroke: "rgba(151,187,205,1)", + data: [28, 48, 40, 19, 86, 27, 90] + }] + }; + + options = { + + scales: + { + yAxes: [ + { + ticks: + { + // beginAtZero:true, + min: -1, + }, + }], + xAxes: [ + { + display: false + }] + }, + + responsive: true, + scaleBeginAtZero: true, + scaleShowGridLines: true, + scaleGridLineColor: "rgba(0,0,0,.05)", + scaleGridLineWidth: 1, + + hover: + { + mode: 'single', + onHover: function(obj) + { + if (obj.length > 0) + tapOrHover(obj[0]._index); + } + }, + + //String - A legend template + legendTemplate: '<ul class="tc-chart-js-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>' + }; + + cv = document.getElementById("eventchart"); + ctx = cv.getContext("2d"); + $timeout(function() + { + var tcGraph2 = new Chart(ctx, + { + type: 'bar', + data: data, + options: options + }); + }); + }); + + //------------------------------------------------------- + // we use this to reload the connkey if authkey changed + //------------------------------------------------------ + + $rootScope.$on("auth-success", function() + { + + NVRDataModel.debug("EventModalCtrl: Re-login detected, resetting everything & re-generating connkey"); + + }); + + //------------------------------------------------------- + // I was kidding, this is where it really is drawn + // scout's promise + //------------------------------------------------------ + + function drawGraphTC(event) + { + + $scope.eid = event.event.Event.Id; + + $scope.alarm_images = []; + + /*data = { + labels: [], + datasets: [ + { + label: 'Score', + fill:true, + borderJoinStyle: 'miter', + pointBorderColor: "rgba(220,220,220,1)", + pointBackgroundColor: "#e74c3c", + backgroundColor: 'rgba(129, 207, 224, 1.0)', + pointHoverRadius: 5, + pointHoverBackgroundColor: "#40d47e", + pointHoverBorderWidth: 2, + tension: 0.1, + borderColor: 'rgba(129, 207, 224, 1.0)', + hoverBackgroundColor: 'rgba(248, 148, 6,1.0)', + hoverBorderColor: 'rgba(248, 148, 6,1.0)', + data: [], + frames: [] + }, + + ] + };*/ + + data = { + labels: [], + datasets: [ + { + label: $translate.instant('kScore'), + fill: true, + backgroundColor: 'rgba(129, 207, 224, 1.0)', + borderColor: 'rgb(92, 147, 159)', + borderCapStyle: 'butt', + borderJoinStyle: 'miter', + pointBorderColor: "rgba(220,220,220,1)", + pointBackgroundColor: "#e74c3c", + + pointHoverRadius: 10, + pointHoverBackgroundColor: "#f39c12", + pointHoverBorderWidth: 1, + tension: 0.1, + + data: [], + frames: [] + }, + + ] + }; + + onlyalarm_data = { + labels: [], + datasets: [ + { + label: $translate.instant('kScore'), + backgroundColor: 'rgba(129, 207, 224, 1.0)', + borderColor: 'rgba(129, 207, 224, 1.0)', + hoverBackgroundColor: 'rgba(248, 148, 6,1.0)', + hoverBorderColor: 'rgba(248, 148, 6,1.0)', + data: [], + frames: [] + }, + + ] + }; + + // Chart.js Options + options = { + + scales: + { + yAxes: [ + { + ticks: + { + // beginAtZero:true, + min: -1, + }, + }], + xAxes: [ + { + display: false + }] + }, + + responsive: true, + scaleBeginAtZero: true, + scaleShowGridLines: true, + scaleGridLineColor: "rgba(0,0,0,.05)", + scaleGridLineWidth: 1, + + hover: + { + mode: 'single', + onHover: function(obj) + { + if (obj.length > 0) + tapOrHover(obj[0]._index); + } + }, + + //String - A legend template + legendTemplate: '<ul class="tc-chart-js-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>' + }; + + $scope.graphWidth = event.event.Frame.length * 10; + if ($scope.graphWidth < $rootScope.devWidth) + $scope.graphWidth = $rootScope.devWidth; + + // NVRDataModel.log ("Changing graph width to " + $scope.graphWidth); + + for (var i = 0; i < event.event.Frame.length; i++) + { + + data.labels.push(event.event.Frame[i].TimeStamp); + //data.labels.push(' '); + data.datasets[0].data.push(event.event.Frame[i].Score); + data.datasets[0].frames.push( + { + x: event.event.Frame[i].TimeStamp, + y: event.event.Frame[i].Score, + eid: event.event.Event.Id, + fid: event.event.Frame[i].FrameId, + //group:i, + relativePath: computeRelativePath(event.event), + score: event.event.Frame[i].Score, + fname: padToN(event.event.Frame[i].FrameId, eventImageDigits) + "-capture.jpg", + + }); + + if (event.event.Frame[i].Type == "Alarm") + { + + onlyalarm_data.labels.push(event.event.Frame[i].TimeStamp); + //data.labels.push(' '); + onlyalarm_data.datasets[0].data.push(event.event.Frame[i].Score); + onlyalarm_data.datasets[0].frames.push( + { + x: event.event.Frame[i].TimeStamp, + y: event.event.Frame[i].Score, + eid: event.event.Event.Id, + fid: event.event.Frame[i].FrameId, + //group:i, + relativePath: computeRelativePath(event.event), + score: event.event.Frame[i].Score, + fname: padToN(event.event.Frame[i].FrameId, eventImageDigits) + "-capture.jpg", + + }); + } + + } + + $scope.dataReady = true; + + cv = document.getElementById("tcchart"); + ctx = cv.getContext("2d"); + + if (NVRDataModel.getLogin().timelineModalGraphType == 'all') + { + btype = 'line'; + current_data = data; + } + else + { + btype = 'bar'; + current_data = onlyalarm_data; + } + $timeout(function() + { + tcGraph = new Chart(ctx, + { + type: btype, + data: current_data, + options: options + }); + }); + + cv.onclick = function(e) + { + var b = tcGraph.getElementAtEvent(e); + if (b.length > 0) + { + tapOrHover(b[0]._index); + } + }; + } + + function tapOrHover(ndx) + { + + $timeout(function() + { + + //console.log ("You tapped " + ndx); + $scope.alarm_images = []; + $scope.playbackURL = NVRDataModel.getLogin().url; + var items = current_data.datasets[0].frames[ndx]; + $scope.alarm_images.push( + { + relativePath: items.relativePath, + fid: items.fid, + fname: items.fname, + score: items.score, + time: moment(items.x).format("MMM D," + NVRDataModel.getTimeFormatSec()), + eid: items.eid + }); + }); + + } + + //-------------------------------------------------------- + // utility function + //-------------------------------------------------------- + + function computeRelativePath(event) + { + var relativePath = ""; + var loginData = NVRDataModel.getLogin(); + var str = event.Event.StartTime; + var yy = moment(str).locale('en').format('YY'); + var mm = moment(str).locale('en').format('MM'); + var dd = moment(str).locale('en').format('DD'); + var hh = moment(str).locale('en').format('HH'); + var min = moment(str).locale('en').format('mm'); + var sec = moment(str).locale('en').format('ss'); + relativePath = event.Event.MonitorId + "/" + + yy + "/" + + mm + "/" + + dd + "/" + + hh + "/" + + min + "/" + + sec + "/"; + return relativePath; + + } + + //-------------------------------------------------------- + // utility function + //-------------------------------------------------------- + + function computeBasePath(event) + { + var basePath = ""; + var loginData = NVRDataModel.getLogin(); + var str = event.Event.StartTime; + var yy = moment(str).locale('en').format('YY'); + var mm = moment(str).locale('en').format('MM'); + var dd = moment(str).locale('en').format('DD'); + var hh = moment(str).locale('en').format('HH'); + var min = moment(str).locale('en').format('mm'); + var sec = moment(str).locale('en').format('ss'); + + basePath = loginData.url + "/events/" + + event.Event.MonitorId + "/" + + yy + "/" + + mm + "/" + + dd + "/" + + hh + "/" + + min + "/" + + sec + "/"; + return basePath; + } + + function padToN(number, digits) + { + + var i; + var stringMax = ""; + var stringLeading = ""; + for (i = 1; i <= digits; i++) + { + stringMax = stringMax + "9"; + if (i != digits) stringLeading = stringLeading + "0"; + } + var numMax = parseInt(stringMax); + + if (number <= numMax) + { + number = (stringLeading + number).slice(-digits); + } + //console.log ("PADTON: returning " + number); + return number; + } + +}]); diff --git a/www/js/FirstUseCtrl.js b/www/js/FirstUseCtrl.js new file mode 100644 index 00000000..1dc6c514 --- /dev/null +++ b/www/js/FirstUseCtrl.js @@ -0,0 +1,95 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console */ + +angular.module('zmApp.controllers').controller('zmApp.FirstUseCtrl', ['$scope', '$ionicSideMenuDelegate', 'zm', '$stateParams', '$ionicHistory', '$state', 'NVRDataModel', '$rootScope', '$ionicPopup', '$translate', function($scope, $ionicSideMenuDelegate, zm, $stateParams, $ionicHistory, $state, NVRDataModel, $rootScope, $ionicPopup, $translate) +{ + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //------------------------------------------------------------------------- + // Controller Main + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + //console.log("**VIEW ** FirstUse Ctrl Entered"); + $ionicSideMenuDelegate.canDragContent(true); + // right up here lets set certs to true, we will disable it later + // this is for first starts + + // + if (window.cordova) + { + cordova.plugins.certificates.trustUnsecureCerts(true); + NVRDataModel.log (">>>>>Accepting all certificates, since its first use"); + } + + + }); + + $scope.switchLang = function() + { + $scope.lang = NVRDataModel.getLanguages(); + $scope.myopt = { + lang: "" + }; + + $rootScope.zmPopup = $ionicPopup.show( + { + scope: $scope, + template: '<ion-radio-fix ng-repeat="item in lang" ng-value="item.value" ng-model="myopt.lang"> {{item.text}} </ion-radio-fix>', + + title: $translate.instant('kSelectLanguage'), + + buttons: [ + { + text: $translate.instant('kButtonCancel'), + onTap: function(e) + { + //return "CANCEL"; + } + + }, + { + text: $translate.instant('kButtonOk'), + onTap: function(e) + { + NVRDataModel.log("Language selected:" + $scope.myopt.lang); + NVRDataModel.setDefaultLanguage($scope.myopt.lang, true); + + //return "OK"; + + } + }] + }); + + }; + + $scope.goToLogin = function() + { + $ionicHistory.nextViewOptions( + { + disableAnimate: false, + disableBack: true + }); + $state.go("login", + { + "wizard": false + }); + return; + }; + + $scope.goToWizard = function() + { + $ionicHistory.nextViewOptions( + { + disableAnimate: false, + disableBack: true + }); + $state.go("wizard"); + return; + }; + +}]); diff --git a/www/js/HelpCtrl.js b/www/js/HelpCtrl.js new file mode 100644 index 00000000..51da877b --- /dev/null +++ b/www/js/HelpCtrl.js @@ -0,0 +1,92 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console, Masonry */ + +angular.module('zmApp.controllers').controller('zmApp.HelpCtrl', ['$scope', '$rootScope', '$ionicModal', 'NVRDataModel', '$ionicSideMenuDelegate', '$ionicHistory', '$state', '$translate', '$q', '$templateRequest', '$sce', '$compile', function($scope, $rootScope, $ionicModal, NVRDataModel, $ionicSideMenuDelegate, $ionicHistory, $state, $translate, $q, $templateRequest, $sce, $compile) +{ + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + //---------------------------------------------------------------- + // This function dynamically inserts the relevant help text file + // based on selected language + //---------------------------------------------------------------- + + function insertHelp() + { + + var l = NVRDataModel.getDefaultLanguage() || 'en'; + var lang = "lang/help/help-" + l + ".html"; + //console.log ("LANG IS " + lang); + var templateUrl = $sce.getTrustedResourceUrl(lang); + var lang_fb = "lang/help/help-" + "en" + ".html"; + var templateUrlFB = $sce.getTrustedResourceUrl(lang_fb); + + $templateRequest(lang) + .then(function(template) + { + var elem = angular.element(document.getElementById('insertHelp')); + $compile(elem.html(template).contents())($scope); + }, + function(error) + { + NVRDataModel.log("Language file " + lang + " not found, falling back"); + $templateRequest(templateUrlFB) + .then(function(template) + { + var elem = angular.element(document.getElementById('insertHelp')); + $compile(elem.html(template).contents())($scope); + }, + function(error) + { + NVRDataModel.log("fallback help not found"); + }); + } + ); + + } + + //------------------------------------------------------------------------- + // Lets make sure we set screen dim properly as we enter + // The problem is we enter other states before we leave previous states + // from a callback perspective in ionic, so we really can't predictably + // reset power state on exit as if it is called after we enter another + // state, that effectively overwrites current view power management needs + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + //console.log("**VIEW ** Help Ctrl Entered"); + NVRDataModel.setAwake(false); + $scope.zmAppVersion = NVRDataModel.getAppVersion(); + insertHelp(); + + }); + +}]); diff --git a/www/js/ImportantMessageCtrl.js b/www/js/ImportantMessageCtrl.js new file mode 100644 index 00000000..0f948d7f --- /dev/null +++ b/www/js/ImportantMessageCtrl.js @@ -0,0 +1,35 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console */ + +angular.module('zmApp.controllers').controller('zmApp.ImportantMessageCtrl', ['$scope', '$ionicSideMenuDelegate', 'zm', '$stateParams', '$timeout', '$rootScope', function($scope, $ionicSideMenuDelegate, zm, $stateParams, $timeout, $rootScope) +{ + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //------------------------------------------------------------------------- + // Controller Main + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + console.log("**VIEW ** LowVersion Ctrl Entered"); + $ionicSideMenuDelegate.canDragContent(true); + $scope.requiredVersion = zm.minAppVersion; + $scope.currentVersion = $stateParams.ver; + $scope.recommendedVersion = zm.recommendedAppVersion; + + }); + + $scope.openMenu = function() + { + $timeout(function() + { + $rootScope.stateofSlide = $ionicSideMenuDelegate.isOpen(); + }, 500); + + $ionicSideMenuDelegate.toggleLeft(); + }; + +}]); diff --git a/www/js/InvalidApiCtrl.js b/www/js/InvalidApiCtrl.js new file mode 100644 index 00000000..1c65b9fd --- /dev/null +++ b/www/js/InvalidApiCtrl.js @@ -0,0 +1,37 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console */ + +angular.module('zmApp.controllers').controller('zmApp.InvalidApiCtrl', ['$scope', '$ionicSideMenuDelegate', 'zm', '$stateParams', '$timeout', '$rootScope', function($scope, $ionicSideMenuDelegate, zm, $stateParams, $timeout, $rootScope) +{ + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //------------------------------------------------------------------------- + // Controller Main + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + console.log("**VIEW ** InvalidAPI Ctrl Entered"); + $ionicSideMenuDelegate.canDragContent(true); + }); + + $scope.openMenu = function() + { + $timeout(function() + { + $rootScope.stateofSlide = $ionicSideMenuDelegate.isOpen(); + }, 500); + + $ionicSideMenuDelegate.toggleLeft(); + }; + + $scope.readFAQ = function() + { + window.open('https://github.com/pliablepixels/zmNinja/wiki/Validating-if-APIs-work-on-ZM', '_blank', 'location=yes'); + return false; + }; + +}]); diff --git a/www/js/LogCtrl.js b/www/js/LogCtrl.js new file mode 100644 index 00000000..808dcbb5 --- /dev/null +++ b/www/js/LogCtrl.js @@ -0,0 +1,315 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global saveAs, cordova,StatusBar,angular,console,moment */ + +angular.module('zmApp.controllers').controller('zmApp.LogCtrl', ['$scope', '$rootScope', 'zm', '$ionicModal', 'NVRDataModel', '$ionicSideMenuDelegate', '$fileLogger', '$cordovaEmailComposer', '$ionicPopup', '$timeout', '$ionicHistory', '$state', '$interval', '$ionicLoading', '$translate', '$http',function($scope, $rootScope, zm, $ionicModal, NVRDataModel, $ionicSideMenuDelegate, $fileLogger, $cordovaEmailComposer, $ionicPopup, $timeout, $ionicHistory, $state, $interval, $ionicLoading, $translate, $http) +{ + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //--------------------------------------------------------------- + // Controller main + //--------------------------------------------------------------- + + var intervalLogUpdateHandle; + + document.addEventListener("pause", onPause, false); + document.addEventListener("resume", onResume, false); + + function onPause() + { + NVRDataModel.debug("LogCtrl: pause called, killing log timer"); + // $interval.cancel(intervalLogUpdateHandle); + } + + function onResume() + { + NVRDataModel.debug("LogCtrl: resume called, starting log timer"); + loadLogs(); + } + + $scope.flipLogs = function() + { + if ($scope.logEntity == 'ZoneMinder') + $scope.logEntity = $rootScope.appName; + else + $scope.logEntity = 'ZoneMinder'; + console.log ("Flipped"); + loadLogs(); + + }; + + $scope.deleteLogs = function() + { + + $rootScope.zmPopup = $ionicPopup.confirm( + { + title: $translate.instant('kPleaseConfirm'), + template: $translate.instant('kDeleteLogsConfirm'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + + $rootScope.zmPopup.then(function(res) + { + if (res) + { + $fileLogger.deleteLogfile().then(function() + { + //console.log('Logfile deleted'); + $fileLogger.setStorageFilename(zm.logFile); + $scope.log.logString = ""; + }); + } + }); + }; + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + //-------------------------------------------------------------------------- + // Make sure user knows information masking is best effort + //-------------------------------------------------------------------------- + + $scope.sendEmail = function(logstring) + { + $ionicPopup.confirm( + { + title: $translate.instant('kSensitiveTitle'), + template: $rootScope.appName + ' ' + $translate.instant('kSensitiveBody'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }) + .then(function(res) + { + if (res) + { + logstring = "Logs for version:" + $scope.zmAppVersion + " ("+$rootScope.platformOS+")\n" + logstring; + sendEmailReally(logstring); + } + + }); + }; + + //-------------------------------------------------------------------------- + // Convenience function to send logs via email + //-------------------------------------------------------------------------- + function sendEmailReally(logstring) + { + if (window.cordova) + { + + // do my best to replace sensitive information + var loginData = NVRDataModel.getLogin(); + + // We don't need this anymore as log and debug now strip passwords + /*if (loginData.password !="") + { + var re1 = new RegExp(loginData.password, "g"); + logstring = logstring.replace(re1, "<deleted>"); + }*/ + // keep the protocol, helps to debug + var urlNoProtocol = loginData.url.replace(/.*?:\/\//, ""); + if (urlNoProtocol != "") + { + var re2 = new RegExp(urlNoProtocol, "g"); + // just replacing baseurl - that will take care of + // masking api but may not be cgi + logstring = logstring.replace(re2, "<server>"); + } + urlNoProtocol = loginData.streamingurl.replace(/.*?:\/\//, ""); + if (urlNoProtocol != "") + { + var re3 = new RegExp(urlNoProtocol, "g"); + logstring = logstring.replace(re3, "<server>"); + } + + urlNoProtocol = loginData.eventServer.replace(/.*?:\/\//, ""); + if (urlNoProtocol != "") + { + var re4 = new RegExp(urlNoProtocol, "g"); + logstring = logstring.replace(re4, "<server>"); + } + + cordova.plugins.email.isAvailable( + function (isAvailable) { + + if (isAvailable) { + cordova.plugins.email.open({ + to: zm.authoremail, + subject: $rootScope.appName + ' logs', + body: logstring + }); + } + else { + // kEmailNotConfigured + $rootScope.zmPopup = SecuredPopups.show('alert', + { + title: $translate.instant('kError'), + template: $translate.instant('kEmailNotConfigured'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + + } + + }); + + + + + // window.plugins.emailComposer.showEmailComposerWithCallback(callback, $rootScope.appName + ' logs', logstring, [zm.authoremail]); + + } + else + { + // console.log("Using default email client to send data"); + + var fname = $rootScope.appName + "-logs-" + + moment().format('MMM-DD-YY_HH-mm-ss') + ".txt"; + + var blob = new Blob([logstring], + { + type: "text/plain;charset=utf-8" + }); + saveAs(blob, fname); + } + + } + + function callback() + { + // console.log ("EMAIL SENT"); + NVRDataModel.debug("Email sent callback called"); + } + + function loadZMlogs() + { + var ld = NVRDataModel.getLogin(); + var lapi = ld.apiurl + "/logs.json?sort=TimeKey&direction=desc&page="+$scope.zmPage; + $http.get (lapi) + .then (function (success) { + $ionicLoading.hide(); + $scope.zmMaxPage = success.data.pagination.pageCount; + console.log ("PAGES="+$scope.zmMaxPage); + var tLogs = ""; + console.log (JSON.stringify(success)); + for (var i=0; i< success.data.logs.length; i++) + { + tLogs = tLogs + moment.unix(success.data.logs[i].Log.TimeKey).format ("MM/DD/YY hh:mm:ss") +" "+ + success.data.logs[i].Log.Code+" " + + success.data.logs[i].Log.Message+"\n"; + } + $scope.log.logString = tLogs; + }, + function (error) { + NVRDataModel.log ("Error getting ZM logs:"+JSON.stringify(error)); + $scope.log.logString = "Error getting log: " + JSON.stringify(error); + + + } ); + + } + + $scope.changePage = function(p) + { + $scope.zmPage = $scope.zmPage + p; + if ($scope.zmPage < 1) $scope.zmPage = 1; + if ($scope.zmPage > $scope.zmMaxPage) $scope.zmPage = $scope.zmMaxPage; + loadLogs(); + }; + + function loadLogs() + { + //console.log ("GETTING LOGS"); + + $ionicLoading.show( + { + template: $translate.instant('kLoading'), + noBackdrop: true, + duration: zm.loadingTimeout + + }); + + if ($scope.logEntity == $rootScope.appName) + { + $fileLogger.getLogfile().then(function(l) + { + + $scope.log.logString = l.split('\n').reverse().join('\n'); + + $ionicLoading.hide(); + }, + function(error) + { + $scope.log.logString = "Error getting log: " + JSON.stringify(error); + $ionicLoading.hide(); + }); + } + else + loadZMlogs(); + + } + + //------------------------------------------------------------------------- + // Lets make sure we set screen dim properly as we enter + // The problem is we enter other states before we leave previous states + // from a callback perspective in ionic, so we really can't predictably + // reset power state on exit as if it is called after we enter another + // state, that effectively overwrites current view power management needs + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + //console.log("**VIEW ** Log Ctrl Entered"); + NVRDataModel.setAwake(false); + $scope.logEntity = $rootScope.appName; + $scope.zmPage = 1; + $scope.zmMaxPage = 1; + + $scope.log = { + logString: "" + }; + + $scope.zmAppVersion = NVRDataModel.getAppVersion(); + + /* intervalLogUpdateHandle = $interval(function () + { + loadLogs(); + + }.bind(this), 3000);*/ + + loadLogs(); + + }); + + $scope.$on('$ionicView.leave', function() + { + //console.log ("Deleting Log interval..."); + // $interval.cancel(intervalLogUpdateHandle); + }); + +}]); diff --git a/www/js/LoginCtrl.js b/www/js/LoginCtrl.js new file mode 100644 index 00000000..300e4fbf --- /dev/null +++ b/www/js/LoginCtrl.js @@ -0,0 +1,860 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console,alert,URI, localforage */ + +angular.module('zmApp.controllers').controller('zmApp.LoginCtrl', ['$scope', '$rootScope', 'zm', '$ionicModal', 'NVRDataModel', '$ionicSideMenuDelegate', '$ionicPopup', '$http', '$q', '$ionicLoading', 'zmAutoLogin', '$cordovaPinDialog', 'EventServer', '$ionicHistory', '$state', '$ionicActionSheet', 'SecuredPopups', '$stateParams', '$translate', function($scope, $rootScope, zm, $ionicModal, NVRDataModel, $ionicSideMenuDelegate, $ionicPopup, $http, $q, $ionicLoading, zmAutoLogin, $cordovaPinDialog, EventServer, $ionicHistory, $state, $ionicActionSheet, SecuredPopups, $stateParams, $translate) +{ + $scope.openMenu = function() + { + + if ($scope.loginData.serverName) + saveItems(false); + $ionicSideMenuDelegate.toggleLeft(); + + }; + + var oldName; + var serverbuttons = []; + var availableServers; + $scope.loginData = NVRDataModel.getLogin(); + + $scope.check = { + isUseAuth: false, + isUseEventServer: false + }; + + $scope.check.isUseAuth = ($scope.loginData.isUseAuth) ? true : false; + $scope.check.isUseEventServer = ($scope.loginData.isUseEventServer == true) ? true : false; + + document.addEventListener("pause", onPause, false); + document.addEventListener("resume", onResume, false); + + function onResume() + { + NVRDataModel.log("Login screen resumed"); + + } + + function onPause() + { + NVRDataModel.log("Login screen going to background, saving data"); + localforage.setItem("settings-temp-data", $scope.loginData); + + } + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + + }; + + //---------------------------------------------------------------- + // Specifies a linked profile to try if this profile fails + //---------------------------------------------------------------- + + $scope.selectFallback = function() + { + var as = Object.keys(NVRDataModel.getServerGroups()); + if (as.length < 2) + { + $rootScope.zmPopup = SecuredPopups.show('alert', + { + title: $translate.instant('kError'), + template: $translate.instant('kFallback2Configs'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + return; + + } + var ab = [ + { + text: $translate.instant('kClear') + }]; + var ld = NVRDataModel.getLogin(); + as.forEach(function(item) + { + if (item != ld.serverName) ab.push( + { + text: item + }); + }); + var sheet = $ionicActionSheet.show( + { + buttons: ab, + titleText: $translate.instant('kSelectFallback'), + cancelText: $translate.instant('kButtonCancel'), + cancel: function() {}, + buttonClicked: function(index) + { + //console.log ("YOU WANT " + ab[index].text + index); + if (index == 0) + $scope.loginData.fallbackConfiguration = ""; + else + $scope.loginData.fallbackConfiguration = ab[index].text; + NVRDataModel.setLogin($scope.loginData); + return true; + } + }); + + }; + + //---------------------------------------------------------------- + // This is called when the user changes profiles + //---------------------------------------------------------------- + + $scope.serverActionSheet = function() + { + var hideSheet = $ionicActionSheet.show( + { + buttons: serverbuttons, + destructiveText: $translate.instant('kDelete'), + titleText: $translate.instant('kManageServerGroups'), + cancelText: $translate.instant('kButtonCancel'), + cancel: function() + { + // add cancel code.. + }, + buttonClicked: function(index) + { + //console.log ("YOU WANT " + serverbuttons[index].text + " INDEX " + index); + + if (serverbuttons[index].text == $translate.instant('kServerAdd') + "...") + { + + $scope.loginData = angular.copy(NVRDataModel.getDefaultLoginObject()); + return true; + } + + var zmServers = NVRDataModel.getServerGroups(); + $scope.loginData = zmServers[serverbuttons[index].text]; + + //console.log ("NEW LOGIN OBJECT IS " + JSON.stringify($scope.loginData)); + + $scope.check.isUseAuth = ($scope.loginData.isUseAuth) ? true : false; + $scope.check.isUseEventServer = ($scope.loginData.isUseEventServer == true) ? true : false; + + NVRDataModel.debug("Retrieved state for this profile:" + JSON.stringify($scope.loginData)); + + // lets make sure Event Server is loaded + // correctly + + // FIXME: But what happens if you don't save? + // loginData gets written but auth is not done + NVRDataModel.setLogin($scope.loginData); + + return true; + }, + + destructiveButtonClicked: function() + { + + if (!$scope.loginData.serverName) + { + NVRDataModel.debug("cannot delete empty entry"); + return true; + + } + $rootScope.zmPopup = SecuredPopups.show('confirm', + { + title: $translate.instant('kDelete'), + template: $translate.instant('kDeleteProfile')+" "+$scope.loginData.serverName, + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }).then(function(res) + { + + if (res) + actuallyDelete(); + + }); + + + + function actuallyDelete() + { + + var zmServers = NVRDataModel.getServerGroups(); + //console.log ("YOU WANT TO DELETE " + $scope.loginData.serverName); + //console.log ("LENGTH OF SERVERS IS " + Object.keys(zmServers).length); + if (Object.keys(zmServers).length > 1) + { + + NVRDataModel.log("Deleting " + $scope.loginData.serverName); + delete zmServers[$scope.loginData.serverName]; + NVRDataModel.setServerGroups(zmServers); + // point to first element + // better than nothing + // note this is actually unordered + $scope.loginData = zmServers[Object.keys(zmServers)[0]]; + NVRDataModel.setLogin($scope.loginData); + + availableServers = Object.keys(NVRDataModel.getServerGroups()); + serverbuttons = [ + { + text: $translate.instant('kServerAdd') + "..." + }]; + for (var servIter = 0; servIter < availableServers.length; servIter++) + { + serverbuttons.push( + { + text: availableServers[servIter] + }); + //console.log("ADDING : " + availableServers[servIter]); + } + //console.log (">>>>>>>delete: server buttons " + JSON.stringify(serverbuttons)); + } + else + { + NVRDataModel.displayBanner('error', [$translate.instant('kBannerCannotDeleteNeedOne')]); + } + + + } + + return true; + } + + }); + }; + + //---------------------------------------------------------------- + // This is when you tap on event server settings + //---------------------------------------------------------------- + + $scope.eventServerSettings = function() + { + NVRDataModel.debug("Saving settings before going to Event Server settings"); + //console.log ( "My loginData saved " + JSON.stringify($scope.loginData)); + NVRDataModel.setLogin($scope.loginData); + $state.go("eventserversettings"); + return; + }; + + //------------------------------------------------------------------------- + // Lets make sure we set screen dim properly as we enter + // The problem is we enter other states before we leave previous states + // from a callback perspective in ionic, so we really can't predictably + // reset power state on exit as if it is called after we enter another + // state, that effectively overwrites current view power management needs + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + //console.log("**VIEW ** LoginCtrl Entered"); + NVRDataModel.setAwake(false); + var ld = NVRDataModel.getLogin(); + oldName = ld.serverName; + + availableServers = Object.keys(NVRDataModel.getServerGroups()); + serverbuttons = [ + { + text: $translate.instant('kServerAdd') + "..." + }]; + for (var servIter = 0; servIter < availableServers.length; servIter++) + { + serverbuttons.push( + { + text: availableServers[servIter] + }); + + //console.log (">>>>>>>ionicview enter: server buttons " + JSON.stringify(serverbuttons)); + } + + NVRDataModel.debug("Does login need to hear the wizard? " + $stateParams.wizard); + + if ($stateParams.wizard == "true") + { + NVRDataModel.log("Creating new login entry for wizard"); + $scope.loginData = angular.copy(NVRDataModel.getDefaultLoginObject()); + $scope.loginData.serverName = $rootScope.wizard.serverName; + $scope.loginData.url = $rootScope.wizard.loginURL; + $scope.loginData.apiurl = $rootScope.wizard.apiURL; + $scope.loginData.streamingurl = $rootScope.wizard.streamingURL; + if ($rootScope.wizard.useauth && $rootScope.wizard.usezmauth) + { + $scope.loginData.username = $rootScope.wizard.zmuser; + $scope.loginData.password = $rootScope.wizard.zmpassword; + } + else + { + $scope.loginData.isUseAuth = false; + } + + if ((/^https:\/\//i.test($scope.loginData.url))) + { + $scope.loginData.useSSL = true; + } + + } + else + { + var savedData; + localforage.getItem("settings-temp-data").then(function(value) + { + savedData = value; + //= zmStorageService.getObject ("settings-temp-data"); + if (!NVRDataModel.isEmpty(savedData)) + { + $scope.loginData = savedData; + NVRDataModel.log("retrieved pre-stored loginData on past pause: " + JSON.stringify($scope.loginData)); + localforage.removeItem("settings-temp-data"); + //zmStorageService.setObject("settings-temp-data", {}); + } + else + { + NVRDataModel.log("Not recovering login data as its empty"); + } + }); + } + + }); + + $scope.$on('$ionicView.beforeLeave', function() + { + //console.log("**VIEW ** LoginCtrl Entered"); + + }); + + //---------------------------------------------------------------- + // We need to make sure that if the user changes a profile, that + // its saved, which involves re-auth. Not doing this will mess + // up monitors. We can't automatically do it, because we really + // don't want re-auth delays each time a user taps on a new profile + // especially if they switch back + // + // So instead, if check if the profile name has changed - if it has + // we block state change and ask the user to save + //---------------------------------------------------------------- + + // credit: http://stackoverflow.com/questions/33385610/ionic-prevent-navigation-on-leave + /* Disabled - seems to crash with native transitions + + $scope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) { + NVRDataModel.setAwake(false); + var ld = NVRDataModel.getLogin(); + + if (ld.serverName != oldName) { + event.preventDefault(); + $rootScope.zmPopup = SecuredPopups.show('alert', { + title: $translate.instant('kPleaseSave'), + template: $translate.instant('kProfileChangeNotification', { + oldName: oldName, + newName: ld.serverName + }) + + }); + + } + });*/ + + $rootScope.$on('$stateChangeSuccess', function() + { + $scope.ignoreDirty = false; + }); + + // Make a noble attempt at deciphering + + //-------------------------------------------------------------------------- + // When PIN is enabled, this is called to specify a PIN + // FIXME: Get rid of cordovaPinDialog. It's really not needed + //-------------------------------------------------------------------------- + $scope.pinPrompt = function(evt) + { + NVRDataModel.log("Password prompt"); + if ($scope.loginData.usePin) + { + $scope.loginData.pinCode = ""; + $cordovaPinDialog.prompt($translate.instant('kEnterPin'), $translate.instant('kPinProtect')).then( + function(result1) + { + + // console.log (JSON.stringify(result1)); + if (result1.input1 && result1.buttonIndex == 1) + { + $cordovaPinDialog.prompt($translate.instant('kReconfirmPin'), $translate.instant('kPinProtect')) + .then(function(result2) + { + if (result1.input1 == result2.input1) + { + NVRDataModel.log("Pin code match"); + $scope.loginData.pinCode = result1.input1; + } + else + { + NVRDataModel.log("Pin code mismatch"); + $scope.loginData.usePin = false; + NVRDataModel.displayBanner('error', [$translate.instant('kBannerPinMismatch')]); + } + }, + function(error) + { + //console.log("Error inside"); + $scope.loginData.usePin = false; + }); + } + else + { + $scope.loginData.usePin = false; + } + }, + function(error) + { + //console.log("Error outside"); + $scope.loginData.usePin = false; + }); + + } + else + { + NVRDataModel.debug("Password disabled"); + } + }; + + //------------------------------------------------------------------------------- + // Makes input easier + //------------------------------------------------------------------------------- + + $scope.portalKeypress = function(evt) + { + + if (/^https:\/\//i.test($scope.loginData.url)) + { + $scope.loginData.useSSL = true; + } + else + { + $scope.loginData.useSSL = false; + } + + if ($scope.loginData.url.slice(-1) == '/') + { + $scope.loginData.apiurl = $scope.loginData.url + "api"; + $scope.loginData.streamingurl = $scope.loginData.url + "cgi-bin"; + } + else + { + $scope.loginData.apiurl = $scope.loginData.url + "/api"; + $scope.loginData.streamingurl = $scope.loginData.url + "/cgi-bin"; + } + + }; + //------------------------------------------------------------------------------- + // Adds http to url if not present + // http://stackoverflow.com/questions/11300906/check-if-a-string-starts-with-http-using-javascript + //------------------------------------------------------------------------------- + function addhttp(url) + { + + if ((!/^(f|ht)tps?:\/\//i.test(url)) && (url != "")) + { + url = "http://" + url; + } + return url; + } + + function addWsOrWss(url) + { + + if ((!/^wss?:\/\//i.test(url)) && (url != "")) + { + url = "ws://" + url; + } + return url; + } + + function endsWith(str, suffix) + { + return str.indexOf(suffix, str.length - suffix.length) !== -1; + } + + //----------------------------------------------------------------------------- + // Perform the login action when the user submits the login form + //----------------------------------------------------------------------------- + + function saveItems(showalert) + { + + //console.log ("*********** SAVE ITEMS CALLED "); + //console.log('Saving login'); + NVRDataModel.setFirstUse(false); + + // used for menu display + + // lets so some basic sanitization of the data + // I am already adding "/" so lets remove spurious ones + // though webkit has no problems. Even so, this is to avoid + // a deluge of folks who look at the error logs and say + // the reason the login data is not working is because + // the app is adding multiple "/" characters + + $scope.loginData.url = $scope.loginData.url.replace(/\s/g, ""); + $scope.loginData.apiurl = $scope.loginData.apiurl.replace(/\s/g, ""); + $scope.loginData.streamingurl = $scope.loginData.streamingurl.replace(/\s/g, ""); + $scope.loginData.eventServer = $scope.loginData.eventServer.replace(/\s/g, ""); + + $scope.loginData.username = $scope.loginData.username.trim(); + + $scope.loginData.isUseAuth = ($scope.check.isUseAuth) ? true : false; + $scope.loginData.isUseEventServer = ($scope.check.isUseEventServer) ? true : false; + + if ($scope.loginData.url.slice(-1) == '/') + { + $scope.loginData.url = $scope.loginData.url.slice(0, -1); + + } + + if ($scope.loginData.apiurl.slice(-1) == '/') + { + $scope.loginData.apiurl = $scope.loginData.apiurl.slice(0, -1); + + } + + if ($scope.loginData.streamingurl.slice(-1) == '/') + { + $scope.loginData.streamingurl = $scope.loginData.streamingurl.slice(0, -1); + + } + + if ($scope.loginData.eventServer.slice(-1) == '/') + { + $scope.loginData.eventServer = $scope.loginData.eventServer.slice(0, -1); + + } + // strip cgi-bin if it is there but only at the end + // Nov 17 Don't mess with this path. centos uses zm-cgi-bin of all things + + /*if ($scope.loginData.streamingurl.slice(-7).toLowerCase() == 'cgi-bin') { + $scope.loginData.streamingurl = $scope.loginData.streamingurl.slice(0, -7); + }*/ + + // check for protocol and if not put it in + + $scope.loginData.url = addhttp($scope.loginData.url); + $scope.loginData.apiurl = addhttp($scope.loginData.apiurl); + $scope.loginData.streamingurl = addhttp($scope.loginData.streamingurl); + $scope.loginData.eventServer = addWsOrWss($scope.loginData.eventServer); + + if ($scope.loginData.useSSL) + { + // replace all http with https + $scope.loginData.url = $scope.loginData.url.replace("http:", "https:"); + $scope.loginData.apiurl = $scope.loginData.apiurl.replace("http:", "https:"); + $scope.loginData.streamingurl = $scope.loginData.streamingurl.replace("http:", "https:"); + $scope.loginData.eventServer = $scope.loginData.eventServer.replace("ws:", "wss:"); + + } + else + { + // replace all https with http + $scope.loginData.url = $scope.loginData.url.replace("https:", "http:"); + $scope.loginData.apiurl = $scope.loginData.apiurl.replace("https:", "http:"); + $scope.loginData.streamingurl = $scope.loginData.streamingurl.replace("https:", "http:"); + // don't do it for WSS - lets mandate that + } + + var apiurl = $scope.loginData.apiurl + '/host/getVersion.json'; + var portalurl = $scope.loginData.url + '/index.php'; + + // Check if isUseAuth is set make sure u/p have a dummy value + if ($scope.check.isUseAuth) + { + if (!$scope.loginData.username) $scope.loginData.username = "x"; + if (!$scope.loginData.password) $scope.loginData.password = "x"; + //NVRDataModel.log("Authentication is disabled, setting dummy user & pass"); + } + + if (parseInt($scope.loginData.maxMontage) <= 0) + { + $scope.loginData.maxMontage = "100"; + } + + // do this before setLogin so message is sent + + if (!$scope.check.isUseEventServer) + { + $rootScope.isAlarm = 0; + if ($rootScope.apnsToken) + { + NVRDataModel.log("Making sure we don't get push notifications"); + EventServer.sendMessage('push', + { + type: 'token', + platform: $rootScope.platformOS, + token: $rootScope.apnsToken, + state: "disabled" + }, 1); + } + } + + NVRDataModel.setLogin($scope.loginData); + + $rootScope.runMode = NVRDataModel.getBandwidth(); + + oldName = $scope.loginData.serverName; + + if ($scope.check.isUseEventServer) + { + EventServer.init(); + if ($rootScope.apnsToken && $scope.loginData.disablePush != true) + { + NVRDataModel.log("Making sure we get push notifications"); + EventServer.sendMessage('push', + { + type: 'token', + platform: $rootScope.platformOS, + token: $rootScope.apnsToken, + state: "enabled" + }, 1); + } + EventServer.sendMessage("control", + { + type: 'filter', + monlist: $scope.loginData.eventServerMonitors, + intlist: $scope.loginData.eventServerInterval + }); + + } + + // lets logout + NVRDataModel.debug("Logging out of current session..."); + $rootScope.authSession = "undefined"; + + + $http( + { + method: 'POST', + timeout:10000, + //withCredentials: true, + url: $scope.loginData.url + '/index.php', + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + + encodeURIComponent(obj[p])); + var params = str.join("&"); + return params; + }, + + data: + { + action: "logout", + view: "login" + } + }) + .finally(function(ans) + { + + zmAutoLogin.doLogin("<button class='button button-clear' style='line-height: normal; min-height: 0; min-width: 0; color:#fff;' ng-click='$root.cancelAuth()'><i class='ion-close-circled'></i> " + $translate.instant('kAuthenticating') + "...</button>") + // Do the happy menu only if authentication works + // if it does not work, there is an emitter for auth + // fail in app.js that will be called to show an error + // box + + .then(function(data) + { + + // Now let's validate if the API works + + // note that due to reachability, it might have switched to another server + + if ($scope.loginData.serverName != NVRDataModel.getLogin().serverName) + { + NVRDataModel.debug(">>> Server information has changed, likely a fallback took over!"); + $scope.loginData = NVRDataModel.getLogin(); + apiurl = $scope.loginData.apiurl + '/host/getVersion.json'; + portalurl = $scope.loginData.url + '/index.php'; + } + + // possible image digits changed between servers + NVRDataModel.getKeyConfigParams(0); + + NVRDataModel.log("Validating APIs at " + apiurl); + $http.get(apiurl) + .success(function(data) + { + + NVRDataModel.getTimeZone(true); + var loginStatus = $translate.instant('kExploreEnjoy') + " " + $rootScope.appName + "!"; + EventServer.refresh(); + + // now grab and report PATH_ZMS + NVRDataModel.getPathZms() + .then(function(data) + { + var ld = NVRDataModel.getLogin(); + var zm_cgi = data.toLowerCase(); + + var user_cgi = (ld.streamingurl).toLowerCase(); + NVRDataModel.log("ZM relative cgi-path: " + zm_cgi + ", you entered: " + user_cgi); + + $http.get(ld.streamingurl + "/zms") + .success(function(data) + { + NVRDataModel.debug("Urk! cgi-path returned success, but it should not have come here"); + loginStatus = $translate.instant('kLoginStatusNoCgi'); + + NVRDataModel.debug("refreshing API version..."); + NVRDataModel.getAPIversion() + .then(function(data) + { + var refresh = NVRDataModel.getMonitors(1); + $rootScope.apiVersion = data; + }, + function(error) + { + var refresh = NVRDataModel.getMonitors(1); + $rootScope.apiVersion = "0.0.0"; + NVRDataModel.debug("Error, failed API version, setting to " + $rootScope.apiVersion); + }); + + if (showalert) + { + $rootScope.zmPopup = SecuredPopups.show('alert', + { + title: $translate.instant('kLoginValidatedTitle'), + template: loginStatus, + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }).then(function(res) + { + + $ionicSideMenuDelegate.toggleLeft(); + NVRDataModel.debug("Force reloading monitors..."); + + }); + } + }) + .error(function(error, status) + { + // If its 5xx, then the cgi-bin path is valid + // if its 4xx then the cgi-bin path is not valid + + if (status < 500) + { + loginStatus = $translate.instant('kLoginStatusNoCgiAlt'); + } + + if (showalert) + { + $rootScope.zmPopup = SecuredPopups.show('alert', + { + title: $translate.instant('kLoginValidatedTitle'), + template: loginStatus, + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }).then(function(res) + { + + $ionicSideMenuDelegate.toggleLeft(); + NVRDataModel.debug("Force reloading monitors..."); + + }); + } + else // make sure CGI error is always shown + { + NVRDataModel.displayBanner((status < 500)? 'error':'info', [loginStatus]); + } + NVRDataModel.debug("refreshing API version..."); + NVRDataModel.getAPIversion() + .then(function(data) + { + var refresh = NVRDataModel.getMonitors(1); + $rootScope.apiVersion = data; + }, + function(error) + { + var refresh = NVRDataModel.getMonitors(1); + $rootScope.apiVersion = "0.0.0"; + NVRDataModel.debug("Error, failed API version, setting to " + $rootScope.apiVersion); + }); + + }); + }); + + }) + .error(function(error) + { + NVRDataModel.displayBanner('error', [$translate.instant('kBannerAPICheckFailed'), $translate.instant('kBannerPleaseCheck')]); + NVRDataModel.log("API login error " + JSON.stringify(error)); + + $rootScope.zmPopup = SecuredPopups.show('alert', + { + title: $translate.instant('kLoginValidAPIFailedTitle'), + template: $translate.instant('kBannerPleaseCheck'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + }); + }); + + }); + } + + // ---------------------------------------------- + // Saves the current profile. Note that + // calling saveItems also updates the defaultServer + //----------------------------------------------- + + $scope.saveItems = function() + { + + if (!$scope.loginData.serverName) + { + $rootScope.zmPopup = $ionicPopup.alert( + { + title: $translate.instant('kError'), + template: $translate.instant('kServerEmptyError'), + }) + .then(function(res) + { + return; + }); + } + else + { + saveItems(true); + availableServers = Object.keys(NVRDataModel.getServerGroups()); + serverbuttons = [ + { + text: $translate.instant('kServerAdd') + "..." + }]; + for (var servIter = 0; servIter < availableServers.length; servIter++) + { + serverbuttons.push( + { + text: availableServers[servIter] + }); + } + //console.log (">>>>>>>ionicview save: server buttons " + JSON.stringify(serverbuttons)); + + } + + }; + +}]); diff --git a/www/js/LowVersionCtrl.js b/www/js/LowVersionCtrl.js new file mode 100644 index 00000000..a7fbb7ff --- /dev/null +++ b/www/js/LowVersionCtrl.js @@ -0,0 +1,24 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console */ + +angular.module('zmApp.controllers').controller('zmApp.LowVersionCtrl', ['$scope', '$ionicSideMenuDelegate', 'zm', '$stateParams', function($scope, $ionicSideMenuDelegate, zm, $stateParams) +{ + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //------------------------------------------------------------------------- + // Controller Main + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + //console.log("**VIEW ** LowVersion Ctrl Entered"); + $ionicSideMenuDelegate.canDragContent(true); + $scope.requiredVersion = zm.minAppVersion; + $scope.currentVersion = $stateParams.ver; + + }); + +}]); diff --git a/www/js/MenuController.js b/www/js/MenuController.js new file mode 100644 index 00000000..071db4df --- /dev/null +++ b/www/js/MenuController.js @@ -0,0 +1,55 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console */ + +angular.module('zmApp.controllers').controller('MenuController', ['$scope', '$ionicSideMenuDelegate', 'zm', '$stateParams', '$ionicHistory', '$state', 'NVRDataModel', '$rootScope', '$ionicPopup', '$translate', function($scope, $ionicSideMenuDelegate, zm, $stateParams, $ionicHistory, $state, NVRDataModel, $rootScope, $ionicPopup, $translate) +{ + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //---------------------------------------------------------------- + // This controller sits along with the main app to bring up + // the language menu from the main menu + //---------------------------------------------------------------- + $scope.switchLang = function() + { + $scope.lang = NVRDataModel.getLanguages(); + $scope.myopt = { + lang: "" + }; + + $rootScope.zmPopup = $ionicPopup.show( + { + scope: $scope, + template: '<ion-radio-fix ng-repeat="item in lang" ng-value="item.value" ng-model="myopt.lang"> {{item.text}} </ion-radio-fix>', + + title: $translate.instant('kSelectLanguage'), + + buttons: [ + { + text: $translate.instant('kButtonCancel'), + onTap: function(e) + { + //return "CANCEL"; + } + + }, + { + text: $translate.instant('kButtonOk'), + onTap: function(e) + { + NVRDataModel.log("Language selected:" + $scope.myopt.lang); + NVRDataModel.setDefaultLanguage($scope.myopt.lang, true); + $rootScope.$emit('language-changed'); + + //return "OK"; + + } + }] + }); + + }; + +}]); diff --git a/www/js/MonitorCtrl.js b/www/js/MonitorCtrl.js new file mode 100644 index 00000000..6a91a52e --- /dev/null +++ b/www/js/MonitorCtrl.js @@ -0,0 +1,550 @@ +/* jshint -W041, -W083 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console */ + +// controller for Monitor View +// refer to comments in EventCtrl for the modal stuff. They are almost the same + +angular.module('zmApp.controllers') + .controller('zmApp.MonitorCtrl', ['$ionicPopup', 'zm', '$scope', 'NVRDataModel', 'message', '$ionicSideMenuDelegate', '$ionicLoading', '$ionicModal', '$state', '$http', '$rootScope', '$timeout', '$ionicHistory', '$ionicPlatform', '$translate', '$q', + function($ionicPopup, zm, $scope, NVRDataModel, message, $ionicSideMenuDelegate, $ionicLoading, $ionicModal, $state, $http, $rootScope, $timeout, $ionicHistory, $ionicPlatform, $translate, $q) + { + + //----------------------------------------------------------------------- + // Controller Main + //----------------------------------------------------------------------- + + // var isModalOpen = false; + + // console.log("***EVENTS: Waiting for Monitors to load before I proceed"); + + var loginData; + + // -------------------------------------------------------- + // Handling of back button in case modal is open should + // close the modal + // -------------------------------------------------------- + + $ionicPlatform.registerBackButtonAction(function(e) + { + e.preventDefault(); + if ($scope.modal != undefined && $scope.modal.isShown()) + { + // switch off awake, as liveview is finished + NVRDataModel.debug("Modal is open, closing it"); + NVRDataModel.setAwake(false); + $scope.modal.remove(); + } + else + { + NVRDataModel.debug("Modal is closed, so toggling or exiting"); + if (!$ionicSideMenuDelegate.isOpenLeft()) + { + $ionicSideMenuDelegate.toggleLeft(); + + } + else + { + navigator.app.exitApp(); + } + + } + + }, 1000); + + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + //----------------------------------------------------------------------- + // This function takes care of changing monitor parameters + // For now, I've only limited it to enable/disable and change monitor mode + // and changing monitor function + //----------------------------------------------------------------------- + $scope.changeConfig = function(monitorName, monitorId, enabled, func) + { + var checked = false; + + if (monitorName == 'All') + { + monitorName = $translate.instant('kAll'); + } + + //console.log("called with " + monitorId + ":" + enabled + ":" + func); + if (enabled == '1') checked = true; + + //if monitorId is not specified, all monitors will be changed + var monitorsIds = []; + if (monitorId == '') + { + for (var i = 0; i < $scope.monitors.length; i++) + { + monitorsIds[i] = $scope.monitors[i].Monitor.Id; + } + } + else + { + monitorsIds[0] = monitorId; + } + + $scope.monFunctions = [ + { + text: $translate.instant('kMonModect'), + value: "Modect" + }, + { + text: $translate.instant('kMonMocord'), + value: "Mocord" + }, + { + text: $translate.instant('kMonRecord'), + value: "Record" + }, + { + text: $translate.instant('kMonNodect'), + value: "Nodect" + }, + { + text: $translate.instant('kMonMonitor'), + value: "Monitor" + }, + { + text: $translate.instant('kMonNone'), + value: "None" + }]; + + $scope.monfunc = { + mymonitorsIds: monitorsIds, + myfunc: func, + myenabled: checked, + myfailedIds: [], + mypromises: [] + }; + + $rootScope.zmPopup = $ionicPopup.show( + { + scope: $scope, + template: '<ion-toggle ng-model="monfunc.myenabled" ng-checked="monfunc.myenabled" toggle-class="toggle-calm">Enabled</ion-toggle><ion-radio-fix ng-repeat="item in monFunctions" ng-value="item.value" ng-model="monfunc.myfunc"> {{item.text}} </ion-radio-fix>', + + title: $translate.instant('kChangeSettingsFor') + ' ' + monitorName, + + buttons: [ + { + text: $translate.instant('kButtonCancel'), + + }, + { + text: $translate.instant('kButtonSave'), + onTap: function(e) + { + $scope.monfunc.mymonitorsIds.forEach(function(item, index) + { + NVRDataModel.debug("MonitorCtrl:changeConfig selection:" + $scope.monfunc.myenabled + + $scope.monfunc.myfunc); + var loginData = NVRDataModel.getLogin(); + var apiRestart = loginData.apiurl + "/states/change/restart.json"; + var apiMon = loginData.apiurl + "/monitors/" + item + ".json"; + + NVRDataModel.debug("MonitorCtrl: URLs for changeConfig save:" + apiMon); + + var isEnabled = ""; + isEnabled = ($scope.monfunc.myenabled == true) ? '1' : '0'; + + $ionicLoading.show( + { + template: $translate.instant('kApplyingChanges') + "...", + noBackdrop: true, + duration: zm.largeHttpTimeout, + }); + + var httpPromise = $http( + { + url: apiMon, + method: 'post', + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': '*/*', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + + encodeURIComponent(obj[p])); + var foo = str.join("&"); + // console.log("****RETURNING " + foo); + NVRDataModel.debug("MonitorCtrl: parmeters constructed: " + foo); + return foo; + }, + data: + { + 'Monitor[Function]': $scope.monfunc.myfunc, + 'Monitor[Enabled]': isEnabled, + } + + }) + .success(function() + { + NVRDataModel.debug("MonitorCtrl: Not restarting ZM - Make sure you have the patch installed in MonitorsController.php or this won't work"); + }) + .error(function(data, status, headers, config) + { + NVRDataModel.debug("MonitorCtrl: Error changing monitor " + JSON.stringify(data)); + $scope.monfunc.myfailedIds.push(item); + }); + + $scope.monfunc.mypromises.push(httpPromise); + }); + + $q.all($scope.monfunc.mypromises).then(function(e) + { + $ionicLoading.hide(); + // if there's a failed ID, an error has occurred + if ($scope.monfunc.myfailedIds.length != 0) + { + $ionicLoading.show( + { + template: $translate.instant('kErrorChangingMonitors') + ". Monitor IDs : " + $scope.monfunc.myfailedIds.toString(), + noBackdrop: true, + duration: 3000, + }); + } + else + { + // I am not restarting ZM after monitor change + /* NVRDataModel.debug ("MonitorCtrl: Restarting ZM"); + $ionicLoading.show({ + template: "Successfully changed Monitor. Please wait, restarting ZoneMinder...", + noBackdrop: true, + duration: zm.largeHttpTimeout, + }); + $http.post(apiRestart) + .then(function (success) { + $ionicLoading.hide(); + var refresh = NVRDataModel.getMonitors(1); + refresh.then(function (data) { + $scope.monitors = data; + $scope.$broadcast('scroll.refreshComplete'); + }); + + }, + function (error) { + $ionicLoading.hide(); + + });*/ + doRefresh(); + } + }); + } + + }, ] + }); + + }; + + // same logic as EventCtrl.js + $scope.finishedLoadingImage = function() + { + // console.log("***Monitor image FINISHED Loading***"); + $ionicLoading.hide(); + }; + + $scope.$on('$ionicView.loaded', function() + { + // console.log("**VIEW ** Monitor Ctrl Loaded"); + }); + + //------------------------------------------------------------------------- + // Lets make sure we set screen dim properly as we enter + // The problem is we enter other states before we leave previous states + // from a callback perspective in ionic, so we really can't predictably + // reset power state on exit as if it is called after we enter another + // state, that effectively overwrites current view power management needs + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + // console.log("**VIEW ** Monitor Ctrl Entered"); + NVRDataModel.setAwake(false); + $ionicSideMenuDelegate.canDragContent(true); + $scope.areImagesLoading = true; + }); + + $scope.$on('$ionicView.afterEnter', function() + { + // console.log("**VIEW ** Monitor Ctrl Entered"); + $scope.monitors = []; + $scope.monitors = message; + + //console.log (">>>>>>>>>>>> MONITOR CTRL " + JSON.stringify($scope.monitors)); + + if ($scope.monitors.length == 0) + { + $rootScope.zmPopup = $ionicPopup.alert( + { + title: $translate.instant('kNoMonitors'), + template: $translate.instant('kPleaseCheckCredentials') + }); + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("login", + { + "wizard": false + }); + return; + } + + loginData = NVRDataModel.getLogin(); + monitorStateCheck(); + //console.log("Setting Awake to " + NVRDataModel.getKeepAwake()); + NVRDataModel.setAwake(NVRDataModel.getKeepAwake()); + // Now lets see if we need to load live screen + + // $rootScope.tappedMid = 1; + if ($rootScope.tappedMid != 0) + { + NVRDataModel.log("Notification tapped, we need to go to monitor " + $rootScope.tappedMid); + + var tm = $rootScope.tappedMid; + $rootScope.tappedMid = 0; + var monitem; + for (var m = 0; m < $scope.monitors.length; m++) + { + if ($scope.monitors[m].Monitor.Id == tm) + { + monitem = $scope.monitors[m]; + break; + } + + } + + openModal(monitem.Monitor.Id, monitem.Monitor.Controllable, monitem.Monitor.ControlId, monitem.Monitor.connKey, monitem); + } + + }); + + $scope.$on('$ionicView.leave', function() + { + // console.log("**VIEW ** Monitor Ctrl Left, force removing modal"); + if ($scope.modal) $scope.modal.remove(); + }); + + $scope.$on('$ionicView.unloaded', function() + { + // console.log("**VIEW ** Monitor Ctrl Unloaded"); + }); + + $scope.openModal = function(mid, controllable, controlid, connKey, monitor) + { + + openModal(mid, controllable, controlid, connKey, monitor); + + }; + + function openModal(mid, controllable, controlid, connKey, monitor) + { + NVRDataModel.debug("MonitorCtrl:Open Monitor Modal with monitor Id=" + mid + + " and Controllable:" + controllable + " with control ID:" + controlid); + + $scope.monitor = monitor; + //console.log (">>>>>>>>>>>> MONITOR CRL " + $scope.monitor. + $scope.monitorId = mid; + $scope.monitorName = NVRDataModel.getMonitorName(mid); + $scope.LoginData = NVRDataModel.getLogin(); + $scope.rand = Math.floor(Math.random() * (999999 - 111111 + 1)) + 111111; + $scope.refMonitor = monitor; + NVRDataModel.log("Monitor Orientation is: " + $scope.orientation); + $rootScope.rand = Math.floor(Math.random() * (999999 - 111111 + 1)) + 111111; + + $scope.showPTZ = false; + $scope.monitorId = mid; + $scope.monitorName = NVRDataModel.getMonitorName(mid); + $scope.controlid = controlid; + + $scope.LoginData = NVRDataModel.getLogin(); + $rootScope.modalRand = Math.floor(Math.random() * (999999 - 111111 + 1)) + 111111; + + $scope.ptzMoveCommand = ""; + $scope.ptzStopCommand = ""; + + $scope.zoomInCommand = ""; + $scope.zoomOutCommand = ""; + $scope.zoomStopCommand = "zoomStop"; + $scope.canZoom = false; + + $scope.presetOn = false; + + $scope.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + $scope.isControllable = controllable; + + $rootScope.modalRand = Math.floor(Math.random() * (999999 - 111111 + 1)) + 111111; + + // This is a modal to show the monitor footage + // We need to switch to always awake if set so the feed doesn't get interrupted + NVRDataModel.setAwake(NVRDataModel.getKeepAwake()); + + $ionicModal.fromTemplateUrl('templates/monitors-modal.html', + { + scope: $scope, + animation: 'slide-in-up' + }) + .then(function(modal) + { + $scope.modal = modal; + + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + "...", + noBackdrop: true, + duration: zm.loadingTimeout + }); + $scope.isModalActive = true; + $scope.modal.show(); + }); + + } + + $scope.closeModal = function() + { + // console.log("Close & Destroy Monitor Modal"); + + // stop networking -nph-zms keeps sucking data + + // switch off awake, as liveview is finished + NVRDataModel.setAwake(false); + $scope.modal.remove(); + $timeout(function() + { + NVRDataModel.log("MonitorCtrl:Stopping network pull..."); + if (NVRDataModel.isForceNetworkStop()) NVRDataModel.stopNetwork("MonitorCtrl-closeModal"); + }, 300); + + }; + //Cleanup the modal when we're done with it! + $scope.$on('$destroy', function() + { + //console.log("Destroy Monitor Modal"); + if ($scope.modal) $scope.modal.remove(); + }); + + //----------------------------------------------------------------------- + // Controller Main + //----------------------------------------------------------------------- + + function monitorStateCheck() + { + var apiMonCheck; + + // The status is provided by zmdc.pl + // "not running", "pending", "running since", "Unable to connect" + var i; + for (i = 0; i < $scope.monitors.length; i++) + { + (function(j) + { + $scope.monitors[j].Monitor.isRunningText = "..."; + $scope.monitors[j].Monitor.isRunning = "..."; + $scope.monitors[j].Monitor.color = zm.monitorCheckingColor; + $scope.monitors[j].Monitor.char = "ion-checkmark-circled"; + apiMonCheck = loginData.apiurl + "/monitors/daemonStatus/id:" + $scope.monitors[j].Monitor.Id + "/daemon:zmc.json"; + + //apiMonCheck = apiMonCheck.replace(loginData.url, $scope.monitors[j].Monitor.baseURL); + + // in multiserver replace apiurl with baseurl + + NVRDataModel.debug("MonitorCtrl:monitorStateCheck: " + apiMonCheck); + //console.log("**** ZMC CHECK " + apiMonCheck); + $http.get(apiMonCheck) + .success(function(data) + { + NVRDataModel.debug("MonitorCtrl: monitor check state returned: " + JSON.stringify(data)); + if (data.statustext.indexOf("not running") > -1) + { + $scope.monitors[j].Monitor.isRunning = "false"; + $scope.monitors[j].Monitor.color = zm.monitorNotRunningColor; + $scope.monitors[j].Monitor.char = "ion-close-circled"; + } + else if (data.statustext.indexOf("pending") > -1) + { + $scope.monitors[j].Monitor.isRunning = "pending"; + $scope.monitors[j].Monitor.color = zm.monitorPendingColor; + } + else if (data.statustext.indexOf("running since") > -1) + { + $scope.monitors[j].Monitor.isRunning = "true"; + $scope.monitors[j].Monitor.color = zm.monitorRunningColor; + } + else if (data.statustext.indexOf("Unable to connect") > -1) + { + $scope.monitors[j].Monitor.isRunning = "false"; + $scope.monitors[j].Monitor.color = zm.monitorNotRunningColor; + $scope.monitors[j].Monitor.char = "ion-close-circled"; + } + + $scope.monitors[j].Monitor.isRunningText = data.statustext; + }) + .error(function(data) + { + NVRDataModel.debug("MonitorCtrl: Error->monitor check state returned: " + + JSON.stringify(data)); + NVRDataModel.displayBanner('error', [$translate.instant('kErrorRetrievingState'), $translate.instant('kPleaseTryAgain')]); + $scope.monitors[j].Monitor.isRunning = "error"; + $scope.monitors[j].Monitor.color = zm.monitorErrorColor; + $scope.monitors[j].Monitor.char = "ion-help-circled"; + }); + + })(i); + } + } + + function doRefresh() + { + $scope.monitors = []; + + var refresh = NVRDataModel.getMonitors(1); + + refresh.then(function(data) + { + $scope.monitors = data; + monitorStateCheck(); + $scope.$broadcast('scroll.refreshComplete'); + }); + } + + $scope.doRefresh = function() + { + //console.log("***Pull to Refresh"); + doRefresh(); + + }; + + } + ]); diff --git a/www/js/MonitorModalCtrl.js b/www/js/MonitorModalCtrl.js new file mode 100644 index 00000000..01169130 --- /dev/null +++ b/www/js/MonitorModalCtrl.js @@ -0,0 +1,1768 @@ +// Common Controller for the montage view +/* jshint -W041 */ +/* jslint browser: true*/ +/* global saveAs, cordova,StatusBar,angular,console,ionic, moment, imagesLoaded, chrome */ + +angular.module('zmApp.controllers').controller('MonitorModalCtrl', ['$scope', '$rootScope', 'zm', 'NVRDataModel', '$ionicSideMenuDelegate', '$timeout', '$interval', '$ionicModal', '$ionicLoading', '$http', '$state', '$stateParams', '$ionicHistory', '$ionicScrollDelegate', '$q', '$sce', 'carouselUtils', '$ionicPopup', 'SecuredPopups', '$translate', function($scope, $rootScope, zm, NVRDataModel, $ionicSideMenuDelegate, $timeout, $interval, $ionicModal, $ionicLoading, $http, $state, $stateParams, $ionicHistory, $ionicScrollDelegate, $q, $sce, carouselUtils, $ionicPopup, SecuredPopups, $translate) +{ + + $scope.animationInProgress = false; + $scope.imageFit = true; + $scope.isModalActive = true; + var intervalModalHandle; + var cycleHandle; + var nphTimer; + var ld = NVRDataModel.getLogin(); + $scope.svgReady = false; + $scope.zoneArray = []; + var originalZones = []; + $scope.isZoneEdit = false; + var _moveStart = false; + var targetID = ""; + $scope.imageZoomable = true; + + + $scope.csize = ($rootScope.platformOS == 'desktop') ? 10:20; + + + window.addEventListener("resize", function(){imageLoaded();}, false); + + + $rootScope.authSession = "undefined"; + + $ionicLoading.show( + { + template: $translate.instant('kNegotiatingStreamAuth') + '...', + animation: 'fade-in', + showBackdrop: true, + duration: zm.loadingTimeout, + maxWidth: 300, + showDelay: 0 + }); + + $scope.currentStreamMode = 'single'; + NVRDataModel.log("Using stream mode " + $scope.currentStreamMode); + + NVRDataModel.debug("MonitorModalCtrl called from " + $ionicHistory.currentStateName()); + + $rootScope.validMonitorId = $scope.monitors[0].Monitor.Id; + NVRDataModel.getAuthKey($rootScope.validMonitorId, $scope.monitors[0].Monitor.connKey) + .then(function(success) + { + $ionicLoading.hide(); + $rootScope.authSession = success; + NVRDataModel.log("Modal: Stream authentication construction: " + $rootScope.authSession); + + }, + function(error) + { + + $ionicLoading.hide(); + NVRDataModel.debug("ModalCtrl: Error details of stream auth:" + error); + //$rootScope.authSession=""; + NVRDataModel.log("Modal: Error returned Stream authentication construction. Retaining old value of: " + $rootScope.authSession); + }); + + $interval.cancel(intervalModalHandle); + $interval.cancel(cycleHandle); + + intervalModalHandle = $interval(function() + { + loadModalNotifications(); + // console.log ("Refreshing Image..."); + }.bind(this), 5000); + + $timeout.cancel(nphTimer); + nphTimer = $timeout(function() + { + $scope.currentStreamMode = 'jpeg'; + NVRDataModel.log("Switching playback via nphzms"); + }, zm.nphSwitchTimer); + + // This is the PTZ menu + + $scope.ptzRadialMenuOptions = { + content: '', + + background: '#2F4F4F', + isOpen: true, + toggleOnClick: false, + button: + { + cssClass: "fa fa-arrows-alt", + }, + items: [ + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: false, + onclick: function() + { + controlPTZ($scope.monitorId, $scope.ptzMoveCommand + 'Down'); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: false, + onclick: function() + { + controlPTZ($scope.monitorId, $scope.ptzMoveCommand + 'DownLeft'); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: false, + + onclick: function() + { + controlPTZ($scope.monitorId, $scope.ptzMoveCommand + 'Left'); + } + }, + { + content: 'D', + empty: true, + + onclick: function() + { + // console.log('About'); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: false, + onclick: function() + { + controlPTZ($scope.monitorId, $scope.ptzMoveCommand + 'UpLeft'); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: false, + onclick: function() + { + controlPTZ($scope.monitorId, $scope.ptzMoveCommand + 'Up'); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: false, + onclick: function() + { + controlPTZ($scope.monitorId, $scope.ptzMoveCommand + 'UpRight'); + } + }, + + { + content: 'H', + empty: true, + onclick: function() + { + //console.log('About'); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: false, + onclick: function() + { + controlPTZ($scope.monitorId, $scope.ptzMoveCommand + 'Right'); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: false, + onclick: function() + { + controlPTZ($scope.monitorId, $scope.ptzMoveCommand + 'DownRight'); + } + }, + + { + content: 'K', + empty: true, + onclick: function() + { + //console.log('About'); + } + }, + ] + }; + + //------------------------------------------------------------- + // On re-auth, we need a new zms + //------------------------------------------------------------- + + $rootScope.$on("auth-success", function() + { + + NVRDataModel.debug("MonitorModalCtrl: Re-login detected, resetting everything & re-generating connkey"); + NVRDataModel.stopNetwork("MonitorModal-auth success"); + $scope.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + + }); + + $scope.cast = function(mid, mon) { + + }; + + //---------------------------------- + // toggles monitor cycling + //---------------------------------- + $scope.toggleCycle = function() + { + //console.log ("HERE"); + $scope.isCycle = !$scope.isCycle; + var ld = NVRDataModel.getLogin(); + ld.cycleMonitors = $scope.isCycle; + NVRDataModel.setLogin(ld); + $scope.cycleText = $scope.isCycle ? $translate.instant('kOn') : $translate.instant('kOff'); + + if ($scope.isCycle) + { + NVRDataModel.log("re-starting cycle timer"); + $interval.cancel(cycleHandle); + + cycleHandle = $interval(function() + { + moveToMonitor($scope.monitorId, 1); + // console.log ("Refreshing Image..."); + }.bind(this), ld.cycleMonitorsInterval * 1000); + } + else + { + NVRDataModel.log("cancelling cycle timer"); + $interval.cancel(cycleHandle); + } + + }; + + //------------------------------------------------------------- + // PTZ enable/disable + //------------------------------------------------------------- + + $scope.togglePTZ = function() + { + + //console.log("PTZ"); + + if ($scope.isControllable == '1') + { + //console.log ("iscontrollable is true"); + $scope.showPTZ = !$scope.showPTZ; + + } + else + { + $ionicLoading.show( + { + template: $translate.instant('kPTZnotConfigured'), + noBackdrop: true, + duration: 3000, + }); + } + + }; + + //------------------------------------------------------------- + // Pause and resume handlers + //------------------------------------------------------------- + + function onPause() + { + NVRDataModel.debug("ModalCtrl: onpause called"); + $interval.cancel(intervalModalHandle); + $interval.cancel(cycleHandle); + // $interval.cancel(modalIntervalHandle); + + // FIXME: Do I need to setAwake(false) here? + } + + function onResume() + { + NVRDataModel.debug("ModalCtrl: Modal resume called"); + if ($scope.isModalActive) + { + NVRDataModel.log("ModalCtrl: Restarting Modal timer on resume"); + + $interval.cancel(intervalModalHandle); + $interval.cancel(cycleHandle); + + var ld = NVRDataModel.getLogin(); + + intervalModalHandle = $interval(function() + { + loadModalNotifications(); + }.bind(this), 5000); + + if (ld.cycleMonitors) + { + NVRDataModel.debug("Cycling enabled at " + ld.cycleMonitorsInterval); + + $interval.cancel(cycleHandle); + + cycleHandle = $interval(function() + { + moveToMonitor($scope.monitorId, 1); + // console.log ("Refreshing Image..."); + }.bind(this), ld.cycleMonitorsInterval * 1000); + + } + + $rootScope.modalRand = Math.floor((Math.random() * 100000) + 1); + + } + + } + + //------------------------------------------------------------- + // Queries the 1.30 API for recording state of current monitor + //------------------------------------------------------------- + function loadModalNotifications() + { + + if (NVRDataModel.versionCompare($rootScope.apiVersion, "1.30") == -1) + { + + return; + } + + if (NVRDataModel.getLogin().enableLowBandwidth) + return; + + var status = [$translate.instant('kMonIdle'), + $translate.instant('kMonPreAlarm'), + $translate.instant('kMonAlarmed'), + $translate.instant('kMonAlert'), + $translate.instant('kMonRecord') + ]; + //console.log ("Inside Modal timer..."); + var apiurl = NVRDataModel.getLogin().apiurl; + var alarmurl = apiurl + "/monitors/alarm/id:" + $scope.monitorId + "/command:status.json"; + NVRDataModel.log("Invoking " + alarmurl); + + $http.get(alarmurl) + .then(function(data) + { + // NVRDataModel.debug ("Success in monitor alarmed status " + JSON.stringify(data)); + + $scope.monStatus = status[parseInt(data.data.status)]; + + }, + function(error) + { + + $scope.monStatus = ""; + NVRDataModel.debug("Error in monitor alarmed status "); + }); + + } + + //------------------------------------------------------------- + // Enable/Disable preset list + //------------------------------------------------------------- + + $scope.togglePresets = function() + { + $scope.presetOn = !$scope.presetOn; + + if ($scope.presetOn) + { + $scope.controlToggle = "hide buttons"; + } + else + { + $scope.controlToggle = "show buttons"; + } + //console.log("Changing preset to " + $scope.presetOn); + + var element = angular.element(document.getElementById("presetlist")); + // bring it in + if ($scope.presetOn) + { + element.removeClass("animated fadeOutUp"); + + } + else + { + element.removeClass("animated fadeInDown"); + element.addClass("animated fadeOutUp"); + } + + }; + + + $scope.saveZones = function() + { + var str=""; + for (var i=0; i < originalZones.length; i++) + { + str = str + "o:"+originalZones[i].coords+"<br/>n:"+$scope.zoneArray[i].coords+"--------------------------------------------------<br/>"; + + } + + $rootScope.zmPopup = SecuredPopups.show('confirm', + { + title: 'Sure', + template: str, + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + + }; + + $scope.changeCircleSize = function() + { + $scope.csize = Math.max (($scope.csize + 5) % 31, 10); + + }; + + $scope.toggleZoneEdit = function() + { + $scope.isZoneEdit = !$scope.isZoneEdit; + + + $scope.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + + + if ($scope.isZoneEdit) + { + $ionicScrollDelegate.$getByHandle("imgscroll").zoomTo(1, true); + $scope.imageZoomable = false; + //document.getElementById("imgscroll").zooming="false"; + + for (var i=0; i < $scope.circlePoints.length; i++) + { + var t = document.getElementById("circle-"+i); + if (t) + { + t.removeEventListener("touchstart",moveStart); + t.removeEventListener("mousedown",moveStart); + //t.removeEventListener("mousemove",moveContinue); + //t.removeEventListener("mouseup",moveStop); + + + t.addEventListener("touchstart",moveStart); + t.addEventListener("mousedown",moveStart); + //t.addEventListener("mousemove",moveContinue); + //t.addEventListener("mouseup",moveStop); + + + console.log ("Found circle-"+i); + } + else + { + console.log ("did not find circle-"+i); + } + + } + } + else // get out of edit + { + + $scope.imageZoomable = true; + } + + }; + + $scope.toggleZone = function() + { + $scope.showZones = !$scope.showZones; + if (!$scope.showZones) + $scope.isZoneEdit = false; + }; + + $scope.imageLoaded = function() + { + imageLoaded(); + }; + + $scope.checkZoom = function() + { + //var z = $ionicScrollDelegate.$getByHandle("imgscroll").getScrollPosition().zoom; + //imageLoaded(); + + }; + + $scope.circleTouch = function (evt) + { + console.log ("TOUCH"); + }; + + //$scope.circleOnDrag = function (evt, ndx) + function recomputePolygons (ax, ay, ndx,z) + { + + + // we get screen X/Y - need to translate + // to SVG points + console.log ("recompute with",ax,"&",ay); + var svg=document.getElementById('zsvg'); + var pt = svg.createSVGPoint(); + pt.x = ax; + pt.y = ay; + var svgP = pt.matrixTransform(svg.getScreenCTM().inverse()); + + $scope.circlePoints[ndx].x = Math.round(svgP.x); + $scope.circlePoints[ndx].y = Math.round(svgP.y); + + // get related polygon set + var zi = $scope.circlePoints[ndx].zoneIndex; + var newPoints=""; + for ( var i=0; i < $scope.circlePoints.length; i++) + { + if ($scope.circlePoints[i].zoneIndex == zi) + { + newPoints = newPoints + " " +$scope.circlePoints[i].x+","+$scope.circlePoints[i].y; + } + console.log ("recomputed polygon:", newPoints); + } + // console.log ("OLD ZONE FOR:"+zi+" is "+$scope.zoneArray[zi].coords ); + //console.log ("NEW ZONE FOR:"+zi+" is "+newPoints); + $scope.zoneArray[zi].coords = newPoints; + + //console.log ("INDEX="+ndx+" DRAG="+svgP.x+":"+svgP.y); + + } + + // credit: http://stackoverflow.com/questions/41411891/most-elegant-way-to-parse-scale-and-re-string-a-string-of-number-co-ordinates?noredirect=1#41411927 + // This function scales coords of zones based on current image size + function scaleCoords(string, sx, sy) { + var f = [sx, sy]; + return string.split(' ').map(function (a) { + return a.split(',').map(function (b, i) { + return Math.round(b * f[i]); + }).join(','); + }).join(' '); + } + + function moveContinue(event) + { + if (!_moveStart) {return;} + + console.log ("CONTINUE: target id="+targetID); + + + /*if(event.preventDefault) event.preventDefault(); + if (event.gesture) event.gesture.preventDefault() ; + if (event.gesture) event.gesture.stopPropagation();*/ + + var x,y; + + var z = $ionicScrollDelegate.$getByHandle("imgscroll").getScrollPosition().zoom; + console.log ("zoom is:"+z); + + //console.log(event, this, "t"); + if (event.touches) + { + //console.log ("TOUCH"); + x = event.targetTouches[0].pageX; + y = event.targetTouches[0].pageY; + + } + else + { + //console.log ("MOUSE"); + x = event.clientX ; + y = event.clientY ; + + + } + + + console.log ("X="+x+" Y="+y + " sl="+document.body.scrollLeft+ " sy="+document.body.scrollTop); + $timeout (function() {recomputePolygons (x,y,targetID,1);}); + + + } + + function moveStop (event) + { + _moveStart = false; + console.log ("STOP"); + } + + function moveStart(event) + { + + _moveStart=true; + targetID = event.target.id.substring(7); + console.log ("START: target id="+targetID); + + if(event.preventDefault) event.preventDefault(); + if (event.gesture) event.gesture.preventDefault() ; + if (event.gesture) event.gesture.stopPropagation(); + + var z = $ionicScrollDelegate.$getByHandle("imgscroll").getScrollPosition().zoom; + console.log ("zoom is:"+z); + + var x,y; + // perhaps event.targetTouches[0]? + if (event.touches) + { + //console.log(event.changedTouches[0], this, "t"); + x = event.touches[0].pageX; + y = event.touches[0].pageY; + + } + else + { + //console.log(event, this, "t"); + x = event.clientX ; + y = event.clientY ; + + } + console.log ("X="+x+" Y="+y + " sl="+document.body.scrollLeft+ " sy="+document.body.scrollTop); + + } + + + // called when the live monitor image loads + // this is a good time to calculate scaled zone points + function imageLoaded() + { + + var img =document.getElementById("singlemonitor"); + + //$scope.cw = img.naturalWidth; + //$scope.ch = img.naturalHeight; + + $scope.cw = img.naturalWidth; + $scope.ch = img.naturalHeight; + + //console.log ("REPORTED DIM:" + $scope.cw+ "x"+$scope.ch ); + //console.log ("ORIGINAL DIM:" + img.naturalWidth+ "x"+img.naturalHeight); + //https://server/zm/api/zones/forMonitor/7.json + // + $scope.zoneArray = []; + $scope.circlePoints = []; + + var ow = $scope.monitor.Monitor.Width; + var oh = $scope.monitor.Monitor.Height; + + // console.log ("MONITOR IS: "+JSON.stringify($scope.monitor)); + + // console.log ("ORIGINAL WH="+ow+"x"+oh); + + for (var i=0; i < originalZones.length; i++) + { + var sx = $scope.cw/ow; + var sy = $scope.ch/oh; + //$scope.zoneArray.push({ + // coords:scaleCoords(originalZones[i].coords,sx,sy), + // type:originalZones[i].type}); + $scope.zoneArray.push({ + coords:originalZones[i].coords, + type:originalZones[i].type}); + + + } + + // now create a points array for circle handles + for (i=0; i < $scope.zoneArray.length; i++) + { + /*jshint loopfunc: true */ + console.log ("ZONE ARRAY="+$scope.zoneArray[i].coords); + $scope.zoneArray[i].coords.split(' ') + .forEach( function(itm) + { + var o=itm.split(','); + $scope.circlePoints.push({x:o[0],y:o[1], zoneIndex:i}); + + // console.log ("CIRCLE X="+o[0]+"Y="+o[1]); + }); + + } + + + + + + + } + + //------------------------------------------------------------- + // this is checked to make sure we are not pulling images + // when app is in background. This is a problem with Android, + // for example + //------------------------------------------------------------- + + $scope.isBackground = function() + { + // console.log ("Is background called from ModalCtrl and returned " + + // NVRDataModel.isBackground()); + return NVRDataModel.isBackground(); + }; + + //------------------------------------------------------------- + // Send PTZ command to ZM + // Note: PTZ fails on desktop, don't bother about it + //------------------------------------------------------------- + + $scope.controlPTZ = function(monitorId, cmd) + { + console.log ("PTZ command is"+cmd); + controlPTZ(monitorId, cmd); + }; + + function controlPTZ(monitorId, cmd) + { + + //presetGotoX + //presetHome + //curl -X POST "http://server.com/zm/index.php?view=request" -d + //"request=control&user=admin&passwd=xx&id=4&control=moveConLeft" + + if ($scope.ptzMoveCommand=="undefined") + { + $ionicLoading.show( + { + template: $translate.instant('kPTZNotReady'), + noBackdrop: true, + duration: 2000, + }); + return; + } + + var ptzData = ""; + if (cmd.lastIndexOf("preset", 0) === 0) + { + NVRDataModel.debug("PTZ command is a preset, so skipping xge/lge"); + ptzData = { + view: "request", + request: "control", + id: monitorId, + control: cmd, + // xge: "30", //wtf + // yge: "30", //wtf + }; + + } + else + { + + ptzData = { + view: "request", + request: "control", + id: monitorId, + control: cmd, + xge: "30", //wtf + yge: "30", //wtf + }; + } + + //console.log("Command value " + cmd + " with MID=" + monitorId); + //console.log("PTZDATA is " + JSON.stringify(ptzData)); + $ionicLoading.hide(); + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + "...", + noBackdrop: true, + duration: zm.loadingTimeout, + }); + + var loginData = NVRDataModel.getLogin(); + $ionicLoading.hide(); + $ionicLoading.show( + { + template: $translate.instant('kSendingPTZ') + "...", + noBackdrop: true, + duration: zm.loadingTimeout, + }); + + var req = $http( + { + method: 'POST', + /*timeout: 15000,*/ + url: loginData.url + '/index.php', + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + + encodeURIComponent(obj[p])); + var foo = str.join("&"); + //console.log("****RETURNING " + foo); + return foo; + }, + + data: ptzData + + }); + + req.success(function(resp) + { + $ionicLoading.hide(); + + }); + + req.error(function(resp) + { + $ionicLoading.hide(); + //console.log("ERROR: " + JSON.stringify(resp)); + NVRDataModel.log("Error sending PTZ:" + JSON.stringify(resp), "error"); + }); + } + + $scope.getZoomLevel = function() + { + //console.log("ON RELEASE"); + var zl = $ionicScrollDelegate.$getByHandle("imgscroll").getScrollPosition(); + //console.log(JSON.stringify(zl)); + }; + + $scope.onTap = function(m, d) + { + + moveToMonitor(m, d); + }; + + $scope.onSwipe = function(m, d) + { + if ($scope.isZoneEdit) + { + NVRDataModel.log ("swipe disabled as you are in edit mode"); + return; + } + var ld = NVRDataModel.getLogin(); + if (!ld.canSwipeMonitors) return; + + if ($ionicScrollDelegate.$getByHandle("imgscroll").getScrollPosition().zoom != 1) + { + //console.log("Image is zoomed in - not honoring swipe"); + return; + } + $scope.monStatus = ""; + moveToMonitor(m, d); + + }; + + function moveToMonitor(m, d) + { + + if ($scope.isZoneEdit) + { + NVRDataModel.log ("Not cycling, as you are editing zones"); + } + var curstate = $ionicHistory.currentStateName(); + var found = 0; + var mid; + mid = NVRDataModel.getNextMonitor(m, d); + + $scope.showPTZ = false; + + // FIXME: clean this up - in a situation where + // no monitors are enabled, will it loop for ever? + do { + mid = NVRDataModel.getNextMonitor(m, d); + m = mid; + //console.log("Next Monitor is " + m); + + found = 0; + for (var i = 0; i < $scope.monitors.length; i++) + { + if ($scope.monitors[i].Monitor.Id == mid && + // if you came from monitors, then ignore noshow + ($scope.monitors[i].Monitor.listDisplay != 'noshow' || curstate == "monitors") && + $scope.monitors[i].Monitor.Function != 'None' && + $scope.monitors[i].Monitor.Enabled != '0') + { + found = 1; + //console.log(mid + "is part of the monitor list"); + NVRDataModel.debug("ModalCtrl: swipe detected, moving to " + mid); + break; + } + else + { + NVRDataModel.debug("skipping " + $scope.monitors[i].Monitor.Id + + " listDisplay=" + $scope.monitors[i].Monitor.listDisplay + + " Function=" + $scope.monitors[i].Monitor.Function + + " Enabled=" + $scope.monitors[i].Monitor.Enabled); + } + } + + } + while (found != 1); + + var slidein; + var slideout; + var dirn = d; + if (dirn == 1) + { + slideout = "animated slideOutLeft"; + slidein = "animated slideInRight"; + } + else + { + slideout = "animated slideOutRight"; + slidein = "animated slideInLeft"; + } + + var element = angular.element(document.getElementById("monitorimage")); + element.addClass(slideout) + .one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', outWithOld); + + function outWithOld() + { + + NVRDataModel.log("ModalCtrl:Stopping network pull..."); + NVRDataModel.stopNetwork("MonitorModal-outwithOld"); + $scope.rand = Math.floor((Math.random() * 100000) + 1); + $scope.animationInProgress = true; + + $timeout(function() + { + element.removeClass(slideout); + element.addClass(slidein) + .one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', inWithNew); + $scope.monitorId = mid; + $scope.monitorName = NVRDataModel.getMonitorName(mid); + $scope.monitor = NVRDataModel.getMonitorObject(mid); + $scope.zoneArray=[]; + $scope.circlePoints=[]; + getZones(); + configurePTZ($scope.monitorId); + }, 200); + } + + function inWithNew() + { + + element.removeClass(slidein); + $scope.animationInProgress = false; + + NVRDataModel.log("New image loaded in"); + var ld = NVRDataModel.getLogin(); + carouselUtils.setStop(false); + if (ld.useNphZms == true) + { + $scope.currentStreamMode = 'single'; + NVRDataModel.log("Setting timer to play nph-zms mode"); + // first 5 seconds, load a snapshot, then switch to real FPS display + // this is to avoid initial image load delay + // FIXME: 5 seconds fair? + $timeout.cancel(nphTimer); + nphTimer = $timeout(function() + { + $scope.currentStreamMode = 'jpeg'; + NVRDataModel.log("Switching playback via nphzms"); + }, zm.nphSwitchTimer); + } + + } + + $ionicLoading.hide(); + + } + + //----------------------------------------------------------------------- + // Sucess/Error handlers for saving a snapshot of the + // monitor image to phone storage + //----------------------------------------------------------------------- + + function SaveSuccess() + { + $ionicLoading.show( + { + template: $translate.instant('kDone'), + noBackdrop: true, + duration: 1000 + }); + NVRDataModel.debug("ModalCtrl:Photo saved successfuly"); + } + + function SaveError(e) + { + $ionicLoading.show( + { + template: $translate.instant('kErrorSave'), + noBackdrop: true, + duration: 2000 + }); + NVRDataModel.log("Error saving image: " + e); + //console.log("***ERROR"); + } + + //------------------------------------------------------------- + // Turns on or off an alarm forcibly (mode true = on, false = off) + //------------------------------------------------------------- + $scope.enableAlarm = function(mid, mode) + { + + if (mode) // trigger alarm + { + $rootScope.zmPopup = SecuredPopups.show('show', + { + title: 'Confirm', + template: $translate.instant('kForceAlarmConfirm') + $scope.monitorName + "?", + buttons: [ + { + text: $translate.instant('kButtonYes'), + onTap: function(e) + { + enableAlarm(mid, mode); + } + }, + { + text: $translate.instant('kButtonNo'), + onTap: function(e) + { + return; + } + }] + + }); + } + else + enableAlarm(mid, mode); + + function enableAlarm(mid, mode) + { + var apiurl = NVRDataModel.getLogin().apiurl; + var c = mode ? "on" : "off"; + var alarmurl = apiurl + "/monitors/alarm/id:" + mid + "/command:" + c + ".json"; + NVRDataModel.log("Invoking " + alarmurl); + + var status = mode ? $translate.instant('kForcingAlarm') : $translate.instant('kCancellingAlarm'); + $ionicLoading.show( + { + template: status, + noBackdrop: true, + duration: zm.largeHttpTimeout, + }); + + $http.get(alarmurl) + .then(function(data) + { + $ionicLoading.show( + { + template: $translate.instant('kSuccess'), + noBackdrop: true, + duration: 2000, + }); + }, + function(error) + { + + $ionicLoading.show( + { + template: $translate.instant('kAlarmAPIError'), + noBackdrop: true, + duration: 3000, + }); + NVRDataModel.debug("Error in enableAlarm " + JSON.stringify(error)); + }); + } + + }; + + //----------------------------------------------------------------------- + // color for monitor state + //----------------------------------------------------------------------- + + $scope.stateColor = function() + { + var status = [$translate.instant('kMonIdle'), + $translate.instant('kMonPreAlarm'), + $translate.instant('kMonAlarmed'), + $translate.instant('kMonAlert'), + $translate.instant('kMonRecord') + ]; + //console.log ("***MONSTATUS**"+$scope.monStatus+"**"); + var color = ""; + switch ($scope.monStatus) + { + case "": + color = "background-color:none"; + break; + case status[0]: + color = "background-color:#4B77BE"; + break; + case status[1]: + color = "background-color:#e67e22"; + break; + case status[2]: + color = "background-color:#D91E18"; + break; + case status[3]: + color = "background-color:#e67e22"; + break; + case status[4]: + color = "background-color:#26A65B"; + break; + } + + return "padding-left:4px;padding-right:4px;" + color; + }; + + //----------------------------------------------------------------------- + // Saves a snapshot of the monitor image to phone storage + //----------------------------------------------------------------------- + + $scope.saveImageToPhoneWithPerms = function(mid) + { + if ($rootScope.platformOS != 'android') + { + saveImageToPhone(mid); + return; + } + + NVRDataModel.debug("ModalCtrl: Permission checking for write"); + var permissions = cordova.plugins.permissions; + permissions.hasPermission(permissions.WRITE_EXTERNAL_STORAGE, checkPermissionCallback, null); + + function checkPermissionCallback(status) + { + if (!status.hasPermission) + { + SaveError("No permission to write to external storage"); + } + permissions.requestPermission(permissions.WRITE_EXTERNAL_STORAGE, succ, err); + } + + function succ(s) + { + saveImageToPhone(mid); + } + + function err(e) + { + SaveError("Error in requestPermission"); + } + }; + + function saveImageToPhone(mid) + { + $ionicLoading.show( + { + template: $translate.instant('kSavingSnapshot') + '...', + noBackdrop: true, + duration: zm.httpTimeout + }); + + NVRDataModel.debug("ModalCtrl: SaveImageToPhone called"); + var canvas, context, imageDataUrl, imageData; + var loginData = NVRDataModel.getLogin(); + var url = loginData.streamingurl + + '/zms?mode=single&monitor=' + mid + + $rootScope.authSession; + NVRDataModel.log("SavetoPhone:Trying to save image from " + url); + + var img = new Image(); + img.onload = function() + { + // console.log("********* ONLOAD"); + canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + context = canvas.getContext('2d'); + context.drawImage(img, 0, 0); + + imageDataUrl = canvas.toDataURL('image/jpeg', 1.0); + imageData = imageDataUrl.replace(/data:image\/jpeg;base64,/, ''); + + if ($rootScope.platformOS != "desktop") + { + try + { + + cordova.exec( + SaveSuccess, + SaveError, + 'Canvas2ImagePlugin', + 'saveImageDataToLibrary', [imageData] + ); + } + catch (e) + { + + SaveError(e.message); + } + } + else + { + + var fname = $scope.monitorName + "-" + + moment().format('MMM-DD-YY_HH-mm-ss') + ".png"; + canvas.toBlob(function(blob) + { + saveAs(blob, fname); + SaveSuccess(); + + }); + } + }; + try + { + img.src = url; + // console.log ("SAVING IMAGE SOURCE"); + } + catch (e) + { + SaveError(e.message); + + } + } + + //------------------------------------------------------------- + //reloaads mon - do we need it? + //------------------------------------------------------------- + + $scope.reloadView = function() + { + NVRDataModel.log("Reloading view for modal view, recomputing rand"); + $rootScope.modalRand = Math.floor((Math.random() * 100000) + 1); + $scope.isModalActive = true; + }; + + $scope.scaleImage = function() + { + + $scope.imageFit = !$scope.imageFit; + if ($scope.imageFit) + $scope.aspectFit="xMidYMid meet"; + else + $scope.aspectFit = "xMidYMid slice"; + + // console.log("Switching image style to " + $scope.imageFit); + }; + + $scope.$on('$ionicView.enter', function() + { + + //https://server/zm/api/zones/forMonitor/X.json + + }); + + $scope.$on('$ionicView.leave', function() + { + // console.log("**MODAL: Stopping modal timer"); + $scope.isModalActive = false; + $interval.cancel(intervalModalHandle); + $interval.cancel(cycleHandle); + }); + + $scope.$on('$ionicView.beforeLeave', function() + { + + NVRDataModel.log("Nullifying the streams..."); + + var element = document.getElementById("singlemonitor"); + if (element) + { + NVRDataModel.debug("Nullifying " + element.src); + element.src = ""; + } + + }); + + $scope.$on('$ionicView.unloaded', function() + { + $scope.isModalActive = false; + + $interval.cancel(intervalModalHandle); + $interval.cance(cycleHandle); + + }); + + $scope.$on('modal.removed', function() + { + $scope.isModalActive = false; + //console.log("**MODAL REMOVED: Stopping modal timer"); + $interval.cancel(intervalModalHandle); + $interval.cancel(cycleHandle); + + NVRDataModel.debug("Modal removed - killing connkey"); + controlStream(17, "", $scope.connKey, -1); + + // Execute action + }); + + //------------------------------------------------------------- + // called to kill connkey, not sure if we really need it + // I think we are calling window.stop() which is a hammer + // anyway + //------------------------------------------------------------- + + function controlStream(cmd, disp, connkey, ndx) + { + // console.log("Command value " + cmd); + + if (disp) + { + $ionicLoading.hide(); + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + '...', + noBackdrop: true, + duration: zm.loadingTimeout, + }); + } + var loginData = NVRDataModel.getLogin(); + + /* + var CMD_NONE = 0; + var CMD_PAUSE = 1; + var CMD_PLAY = 2; + var CMD_STOP = 3; + var CMD_FASTFWD = 4; + var CMD_SLOWFWD = 5; + var CMD_SLOWREV = 6; + var CMD_FASTREV = 7; + var CMD_ZOOMIN = 8; + var CMD_ZOOMOUT = 9; + var CMD_PAN = 10; + var CMD_SCALE = 11; + var CMD_PREV = 12; + var CMD_NEXT = 13; + var CMD_SEEK = 14; + var CMD_QUIT = 17; + var CMD_QUERY = 99; + */ + + var myauthtoken = $rootScope.authSession.replace("&auth=", ""); + //&auth= + var req = $http( + { + method: 'POST', + /*timeout: 15000,*/ + url: loginData.url + '/index.php', + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', + //'Accept': '*/*', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + + encodeURIComponent(obj[p])); + var foo = str.join("&"); + //console.log("****RETURNING " + foo); + return foo; + }, + + data: + { + view: "request", + request: "stream", + connkey: connkey, + command: cmd, + auth: myauthtoken, + + } + }); + req.success(function(resp) + { + + if (resp.result == "Ok" && ndx != -1) + { + var ld = NVRDataModel.getLogin(); + var apiurl = ld.apiurl + "/events/" + resp.status.event + ".json"; + //console.log ("API " + apiurl); + $http.get(apiurl) + .success(function(data) + { + if ($scope.MontageMonitors[ndx].eventUrlTime != data.event.Event.StartTime) + { + + var element = angular.element(document.getElementById($scope.MontageMonitors[ndx].Monitor.Id + "-timeline")); + element.removeClass('animated slideInRight'); + element.addClass('animated slideOutRight'); + $timeout(function() + { + element.removeClass('animated slideOutRight'); + element.addClass('animated slideInRight'); + $scope.MontageMonitors[ndx].eventUrlTime = data.event.Event.StartTime; + }, 300); + + } + + }) + .error(function(data) + { + $scope.MontageMonitors[ndx].eventUrlTime = "-"; + }); + + } + + }); + + req.error(function(resp) + { + //console.log("ERROR: " + JSON.stringify(resp)); + NVRDataModel.log("Error sending event command " + JSON.stringify(resp), "error"); + }); + } + + $scope.toggleListMenu = function() + { + + + $scope.isToggleListMenu = !$scope.isToggleListMenu; + //console.log ("isToggleListMenu:"+$scope.isToggleListMenu); + }; + + //------------------------------------------------------------- + // Zoom in and out via +- for desktops + //------------------------------------------------------------- + $scope.zoomImage = function(val) + { + + if ($scope.isZoneEdit) + { + $ionicLoading.show( + { + //template: $translate.instant('kError'), + template: 'zoom disabled in zone edit mode', + noBackdrop: true, + duration: 2000 + }); + + return; + } + var zl = parseInt($ionicScrollDelegate.$getByHandle("imgscroll").getScrollPosition().zoom); + if (zl == 1 && val == -1) + { + NVRDataModel.debug("Already zoomed out max"); + return; + } + + zl += val; + NVRDataModel.debug("Zoom level is " + zl); + $ionicScrollDelegate.$getByHandle("imgscroll").zoomTo(zl, true); + + }; + + //------------------------------------------------------------- + // Retrieves PTZ state for each monitor + //------------------------------------------------------------- + // make sure following are correct: + // $scope.isControllable + // $scope.controlid + // + function configurePTZ(mid) + { + $scope.presetAndControl = $translate.instant('kMore'); + $scope.ptzWakeCommand = ""; + $scope.ptzSleepCommand = ""; + $scope.ptzResetCommand = ""; + + $scope.ptzMoveCommand = "undefined"; + $scope.ptzStopCommand = ""; + + $scope.zoomInCommand = ""; + $scope.zoomOutCommand = ""; + $scope.zoomStopCommand = "zoomStop"; + $scope.canZoom = false; + + $scope.presetOn = true; + $scope.controlToggle = "hide buttons"; + + NVRDataModel.debug("configurePTZ: called with mid=" + mid); + var ld = NVRDataModel.getLogin(); + var url = ld.apiurl + "/monitors/" + mid + ".json"; + $http.get(url) + .success(function(data) + { + $scope.isControllable = data.monitor.Monitor.Controllable; + + // *** Only for testing - comment out // + //$scope.isControllable = '1'; + // for testing only + // $scope.isControllable = 1; + $scope.controlid = data.monitor.Monitor.ControlId; + if ($scope.isControllable == '1') + { + + var apiurl = NVRDataModel.getLogin().apiurl; + var myurl = apiurl + "/controls/" + $scope.controlid + ".json"; + NVRDataModel.debug("configurePTZ : getting controllable data " + myurl); + + $http.get(myurl) + .success(function(data) + { + + // *** Only for testing - comment out - start// + /*data.Control.Control.CanSleep = '1'; + data.Control.Control.CanWake = '1'; + data.Control.Control.CanReset = '1'; + data.Control.Control.CanZoom = '1'; + data.control.Control.HasPresets = '1'; + data.control.Control.HasHomePreset = '1';*/ + // *** Only for testing - comment out - end // + + $scope.ptzMoveCommand = "move"; // start with as move; + $scope.ptzStopCommand = ""; + + console.log ("GOT CONTROL "+JSON.stringify(data.control.Control)); + + if (data.control.Control.CanZoom == '1') + { + $scope.canZoom = true; + if (data.control.Control.CanZoomCon == '1') + { + $scope.zoomInCommand = "zoomConTele"; + $scope.zoomOutCommand = "zoomConWide"; + + } + else if (data.control.Control.CanZoomRel == '1') + { + $scope.zoomInCommand = "zoomRelTele"; + $scope.zoomOutCommand = "zoomRelWide"; + } + else if (data.control.Control.CanZoomAbs == '1') + { + $scope.zoomInCommand = "zoomRelAbs"; + $scope.zoomOutCommand = "zoomRelAbs"; + } + } + + NVRDataModel.debug("configurePTZ: control data returned " + JSON.stringify(data)); + + + if (data.control.Control.CanMoveMap == '1') + { + + //seems moveMap uses Up/Down/Left/Right, + // so no prefix + $scope.ptzMoveCommand = ""; + $scope.ptzStopCommand = "moveStop"; + console.log ("MoveAbs set"); + } + + if (data.control.Control.CanMoveAbs == '1') + { + + $scope.ptzMoveCommand = "moveAbs"; + $scope.ptzStopCommand = "moveStop"; + console.log ("MoveAbs set"); + } + + if (data.control.Control.CanMoveRel == '1') + { + + $scope.ptzMoveCommand = "moveRel"; + $scope.ptzStopCommand = "moveStop"; + } + + + + // Prefer con over rel if both enabled + // I've tested con + + if (data.control.Control.CanMoveCon == '1') + { + + $scope.ptzMoveCommand = "moveCon"; + $scope.ptzStopCommand = "moveStop"; + } + //CanMoveMap + + // presets + NVRDataModel.debug("ConfigurePTZ Preset value is " + data.control.Control.HasPresets); + $scope.ptzPresets = []; + + if (data.control.Control.HasPresets == '1') + { + //$scope.presetAndControl = $translate.instant('kPresets'); + + $scope.ptzPresetCount = parseInt(data.control.Control.NumPresets); + //$scope.ptzPresetCount =80; + + NVRDataModel.debug("ConfigurePTZ Number of presets is " + $scope.ptzPresetCount); + + for (var p = 0; p < $scope.ptzPresetCount; p++) + { + $scope.ptzPresets.push( + { + name: (p + 1).toString(), + icon: '', + cmd: "presetGoto" + (p + 1).toString(), + style: 'button-royal' + }); + + } + + if (data.control.Control.HasHomePreset == '1') + { + $scope.ptzPresets.unshift( + { + name: '', + icon: "ion-ios-home", + cmd: 'presetHome', + style: 'button-royal' + }); + + } + + } + /*else + { + $scope.presetAndControl = $translate.instant('kMore'); + }*/ + // lets add these to the end + // strictly speaking, they aren't really presets, but meh for now + + // no need to darken these buttons if presets are not there + var buttonAccent = "button-dark"; + if ($scope.ptzPresets.length == 0) + { + buttonAccent = ""; + } + + if (data.control.Control.CanWake == '1') + { + + $scope.ptzPresets.push( + { + name: 'W', + icon: "ion-eye", + cmd: 'wake', + style: 'button-royal ' + buttonAccent + }); + + } + + if (data.control.Control.CanSleep == '1') + { + $scope.ptzPresets.push( + { + name: 'S', + icon: "ion-eye-disabled", + cmd: 'sleep', + style: 'button-royal ' + buttonAccent + }); + + } + + if (data.control.Control.CanReset == '1') + { + $scope.ptzPresets.push( + { + name: 'R', + icon: "ion-ios-loop-strong", + cmd: 'reset', + style: 'button-royal ' + buttonAccent + }); + + } + + NVRDataModel.log("ConfigurePTZ Modal: ControlDB reports PTZ command to be " + $scope.ptzMoveCommand); + }) + .error(function(data) + { + // console.log("** Error retrieving move PTZ command"); + NVRDataModel.log("ConfigurePTZ : Error retrieving PTZ command " + JSON.stringify(data), "error"); + }); + + } + else + { + NVRDataModel.log("configurePTZ " + mid + " is not PTZ controllable"); + } + }) + .error(function(data) + { + // console.log("** Error retrieving move PTZ command"); + NVRDataModel.log("configurePTZ : Error retrieving PTZ command " + JSON.stringify(data), "error"); + }); + + } + + function getZones() + { + //https://server/zm/api/zones/forMonitor/7.json + var api = NVRDataModel.getLogin().apiurl+"/zones/forMonitor/"+$scope.monitorId+".json"; + NVRDataModel.debug ("Getting zones using:"+api); + originalZones = []; + $http.get (api) + .then (function (succ) { + console.log (JSON.stringify(succ)); + for (var i=0; i < succ.data.zones.length; i++) + { + originalZones.push ({ + coords:succ.data.zones[i].Zone.Coords, + area: succ.data.zones[i].Zone.Area, + type:succ.data.zones[i].Zone.Type}); + } + + }, + function (err) { + NVRDataModel.debug ("Error getting zones :"+JSON.stringify(err)); + + }); + + } + + $scope.$on('modal.shown', function() + { + + $scope.monStatus = ""; + $scope.isToggleListMenu = true; + //console.log (">>>>>>>>>>>>>>>>>>>STOOOP"); + document.addEventListener("pause", onPause, false); + document.addEventListener("resume", onResume, false); + + /*document.addEventListener("mouseup", moveStop, false); + document.addEventListener("touchend", moveStop, false); + + document.addEventL`istener("mousemove", moveContinue, false); + document.addEventListener("touchmove", moveContinue, false);*/ + + + + + $scope.showZones = false; + + getZones(); + + var ld = NVRDataModel.getLogin(); + //currentEvent = $scope.currentEvent; + $scope.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + //console.log ("************* GENERATED CONNKEY " + $scope.connKey); + $scope.currentFrame = 1; + $scope.monStatus = ""; + $scope.isCycle = ld.cycleMonitors; + $scope.cycleText = $scope.isCycle ? $translate.instant('kOn') : $translate.instant('kOff'); + + $scope.quality = (NVRDataModel.getBandwidth() == "lowbw") ? zm.monSingleImageQualityLowBW : ld.monSingleImageQuality; + + configurePTZ($scope.monitorId); + + if (ld.cycleMonitors) + { + NVRDataModel.debug("Cycling enabled at " + ld.cycleMonitorsInterval); + + $interval.cancel(cycleHandle); + + cycleHandle = $interval(function() + { + moveToMonitor($scope.monitorId, 1); + // console.log ("Refreshing Image..."); + }.bind(this), ld.cycleMonitorsInterval * 1000); + + } + + }); + +}]); diff --git a/www/js/MontageCtrl.js b/www/js/MontageCtrl.js new file mode 100644 index 00000000..e2e26c00 --- /dev/null +++ b/www/js/MontageCtrl.js @@ -0,0 +1,2024 @@ +// Controller for the montage view +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console,ionic,Packery, Draggabilly, imagesLoaded, ConnectSDK, moment */ + +angular.module('zmApp.controllers') + .controller('zmApp.MontageCtrl', ['$scope', '$rootScope', 'NVRDataModel', 'message', '$ionicSideMenuDelegate', '$timeout', '$interval', '$ionicModal', '$ionicLoading', '$http', '$state', '$ionicPopup', '$stateParams', '$ionicHistory', '$ionicScrollDelegate', '$ionicPlatform', 'zm', '$ionicPopover', '$controller', 'imageLoadingDataShare', '$window', '$localstorage', '$translate', 'SecuredPopups', function($scope, $rootScope, NVRDataModel, message, $ionicSideMenuDelegate, $timeout, $interval, $ionicModal, $ionicLoading, $http, $state, $ionicPopup, $stateParams, $ionicHistory, $ionicScrollDelegate, $ionicPlatform, zm, $ionicPopover, $controller, imageLoadingDataShare, $window, $localstorage, $translate, SecuredPopups) + { + + //--------------------------------------------------------------------- + // Controller main + //--------------------------------------------------------------------- + + var intervalHandleMontage; // image re-load handler + var intervalHandleAlarmStatus; // status of each alarm state + var intervalHandleMontageCycle; + + var gridcontainer; + var pckry, draggie; + var draggies; + var loginData; + var timestamp; + var sizeInProgress; + var modalIntervalHandle; + var ld; + var refreshSec; + + $rootScope.$on("auth-success", function () { + NVRDataModel.debug("REAUTH"); + console.log ("RETAUTH"); + NVRDataModel.stopNetwork(); + }); + + + //-------------------------------------------------------------------------------------- + // Handles bandwidth change, if required + // + //-------------------------------------------------------------------------------------- + + $rootScope.$on("bandwidth-change", function(e, data) + { + // not called for offline, I'm only interested in BW switches + NVRDataModel.debug("Got network change:" + data); + var ds; + if (data == 'lowbw') + { + ds = $translate.instant('kLowBWDisplay'); + } + else + { + ds = $translate.instant('kHighBWDisplay'); + } + NVRDataModel.displayBanner('net', [ds]); + var ld = NVRDataModel.getLogin(); + refreshSec = (NVRDataModel.getBandwidth() == 'lowbw') ? ld.refreshSecLowBW : ld.refreshSec; + + $interval.cancel(intervalHandleMontage); + $interval.cancel(intervalHandleMontageCycle); + + + intervalHandleMontage = $interval(function() + { + loadNotifications(); + }.bind(this), refreshSec * 1000); + + intervalHandleMontageCycle = $interval(function() + { + cycleMontageProfiles(); + }.bind(this), NVRDataModel.getLogin().cycleMontageInterval* 1000); + + if (NVRDataModel.getBandwidth() == 'lowbw') + { + NVRDataModel.debug("Enabling low bandwidth parameters"); + $scope.LoginData.montageQuality = zm.montageQualityLowBW; + $scope.LoginData.singleImageQuality = zm.eventSingleImageQualityLowBW; + $scope.LoginData.montageHistoryQuality = zm.montageQualityLowBW; + + } + }); + + // -------------------------------------------------------- + // Handling of back button in case modal is open should + // close the modal + // -------------------------------------------------------- + + $ionicPlatform.registerBackButtonAction(function(e) + { + e.preventDefault(); + if ($scope.modal != undefined && $scope.modal.isShown()) + { + // switch off awake, as liveview is finished + NVRDataModel.debug("Modal is open, closing it"); + NVRDataModel.setAwake(false); + $scope.isModalActive = false; + cleanupOnClose(); + } + else + { + NVRDataModel.debug("Modal is closed, so toggling or exiting"); + if (!$ionicSideMenuDelegate.isOpenLeft()) + { + $ionicSideMenuDelegate.toggleLeft(); + + } + else + { + navigator.app.exitApp(); + } + + } + + }, 1000); + + /*$scope.toggleHide = function(mon) + { + + + if (mon.Monitor.listDisplay == 'noshow') + mon.Monitor.listDisplay = 'show'; + else + mon.Monitor.listDisplay = 'noshow'; + + + + };*/ + + // called by afterEnter to load Packery + function initPackery() + { + + $ionicLoading.show( + { + template: $translate.instant('kArrangingImages'), + noBackdrop: true, + duration: zm.loadingTimeout + }); + + var progressCalled = false; + draggies = []; + var layouttype = true; + var ld = NVRDataModel.getLogin(); + + var positionsStr = ld.packeryPositions; + var positions = {}; + + if (positionsStr == '' || positionsStr == undefined) + { + NVRDataModel.log("Did NOT find a packery layout"); + layouttype = true; + } + else + { + + //console.log ("POSITION STR IS " + positionsStr); + positions = JSON.parse(positionsStr); + NVRDataModel.log("found a packery layout"); + + layouttype = false; + } + + var cnt = 0; + $scope.MontageMonitors.forEach(function(elem) + { + if ((elem.Monitor.Enabled != '0') && (elem.Monitor.Function != 'None') ) + cnt++; + }); + + NVRDataModel.log("Monitors that are active and not DOM hidden: " + cnt + " while grid has " + positions.length); + + if (cnt > NVRDataModel.getLogin().maxMontage) + { + cnt = NVRDataModel.getLogin().maxMontage; + NVRDataModel.log("restricting monitor count to " + cnt + " due to max-montage setting"); + } + + if (cnt != positions.length) + { + + NVRDataModel.log("Whoops!! Monitors have changed. I'm resetting layouts, sorry!"); + layouttype = true; + positions = {}; + } + + var elem = angular.element(document.getElementById("mygrid")); + + //console.log ("**** mygrid is " + JSON.stringify(elem)); + + pckry = new Packery('.grid', + { + itemSelector: '.grid-item', + percentPosition: true, + columnWidth: '.grid-sizer', + gutter: 0, + initLayout: layouttype, + shiftPercentResize: true + + }); + + imagesLoaded(elem).on('progress', function(instance, img) + { + + var result = img.isLoaded ? 'loaded' : 'broken'; + NVRDataModel.debug('~~loaded image is ' + result + ' for ' + img.img.src); + + // lay out every image if a pre-arranged position has not been found + + $timeout(function() + { + if (layouttype) pckry.layout(); + }, 100); + + progressCalled = true; + + // if (layouttype) $timeout (function(){layout(pckry);},100); + }); + + imagesLoaded(elem).on('always', function() + { + //console.log ("******** ALL IMAGES LOADED"); + // $scope.$digest(); + NVRDataModel.debug("All images loaded"); + + $scope.areImagesLoading = false; + + $ionicLoading.hide(); + + if (!progressCalled) + { + NVRDataModel.log("*** PROGRESS WAS NOT CALLED"); + // pckry.reloadItems(); + } + + $timeout(function() + { + + pckry.getItemElements().forEach(function(itemElem) + { + + draggie = new Draggabilly(itemElem); + pckry.bindDraggabillyEvents(draggie); + draggies.push(draggie); + draggie.disable(); + draggie.unbindHandles(); + }); + + pckry.on('dragItemPositioned', itemDragged); + + if (!isEmpty(positions)) + { + NVRDataModel.log("Arranging as per packery grid"); + + + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + for (var j = 0; j < positions.length; j++) + { + if ($scope.MontageMonitors[i].Monitor.Id == positions[j].attr) + { + if (isNaN(positions[j].size)) positions[j].size=20; + $scope.MontageMonitors[i].Monitor.gridScale = positions[j].size; + $scope.MontageMonitors[i].Monitor.listDisplay = positions[j].display; + NVRDataModel.debug("Setting monitor ID: " + $scope.MontageMonitors[i].Monitor.Id + " to size: " + positions[j].size + " and display:" + positions[j].display); + } + //console.log ("Index:"+positions[j].attr+ " with size: " + positions[j].size); + } + } + + NVRDataModel.debug("All images loaded, doing image layout"); + + } + $timeout(function() + { + //NVRDataModel.log("Force calling resize"); + ///pckry.reloadItems(); + ///positions is defined only if layouttype was false + console.log (">>> Positions is "+ JSON.stringify(positions)); + if (!layouttype) pckry.initShiftLayout(positions, "data-item-id"); + // now do a jiggle + $timeout(function() + { + NVRDataModel.debug("doing the jiggle and dance..."); + pckry.resize(true); + }, 300); + + }, 100); + + //pckry.onresize(); + + }, 20); + + }); + + function itemDragged(item) + { + NVRDataModel.debug("drag complete"); + $timeout (function(){pckry.shiftLayout();},20); + + pckry.once ('layoutComplete', function() { + + var positions = pckry.getShiftPositions('data-item-id'); + //console.log ("POSITIONS MAP " + JSON.stringify(positions)); + var ld = NVRDataModel.getLogin(); + ld.packeryPositions = JSON.stringify(positions); + console.log ("Saving " + ld.packeryPositions); + // console.log ("FULL OBJECT "+ JSON.stringify(ld)); + ld.currentMontageProfile = ""; + $scope.currentProfileName = $translate.instant ('kMontage'); + NVRDataModel.setLogin(ld); + NVRDataModel.debug("saved new positions"); + }); + + //pckry.getItemElements().forEach(function (itemElem) { + + //console.log (itemElem.attributes['data-item-id'].value+" size "+itemElem.attributes['data-item-size'].value ); + // }); + + + } + + } + + function isEmpty(obj) + { + for (var prop in obj) + { + return false; + } + return true; + } + + //----------------------------------------------------------------------- + // color for monitor state in montage + //----------------------------------------------------------------------- + + $scope.stateColor = function() + { + //console.log ("***MONSTATUS**"+$scope.monStatus+"**"); + var attr = ""; + switch ($scope.monStatus) + { + case "": + attr = "color:rgba(0, 0, 0, 0)"; + break; + case "idle": + attr = "color:rgba(0, 0, 0, 0)"; + break; + case "pre-alarm": + attr = "color:#e67e22"; + break; + case "alarmed": + attr = "color:#D91E18"; + break; + case "alert": + attr = "color:#e67e22"; + break; + case "record": + attr = "color:#26A65B"; + break; + } + + return attr; + }; + + + function findNext (key,obj) + { + + console.log (" key is: "+ key); + console.log ("array is " + JSON.stringify (obj)); + var keys = Object.keys(obj); + + var len = keys.length; + var curindex = keys.indexOf(key); + var modulus = (curindex + 1) % len; + + console.log ("*********** len="+len+" curr="+curindex+" next="+modulus); + + //console.log ("Keys array "+ JSON.stringify(keys)); + + //console.log ("Current index: "+ keys.indexOf(key) ); + //console.log ("returning index of " + (keys.indexOf(key) + 1) % (keys.length)); + // console.log ("keys length is "+ keys.length); + return keys[modulus]; + + /* var size = Object.keys(obj).length; + var i; + for (i=0; i<size; i++) + { + if (Object.keys(obj)[i] == key) + break; + } + i = (i + 1) % size; + return Object.keys(obj)[i];*/ + + } + + //---------------------------------------------- + // cycle profiles + //----------------------------------------------- + + function cycleMontageProfiles() + { + + var ld = NVRDataModel.getLogin(); + + if (!ld.cycleMontageProfiles) + { + // NVRDataModel.debug ("cycling disabled"); + return; + + } + + if ($scope.reOrderActive) + { + NVRDataModel.debug ("not cycling, re-order in progress"); + return; + } + + if ($scope.isDragabillyOn) + { + NVRDataModel.debug ("not cycling, edit in progress"); + return; + + } + + var nextProfile = findNext(ld.currentMontageProfile, ld.packeryPositionsArray); + + if (nextProfile == ld.currentMontageProfile) + { + NVRDataModel.debug ("Not cycling profiles, looks like you only have one"); + } + else + { + NVRDataModel.debug ("Cycling profile from: "+ld.currentMontageProfile+" to:"+nextProfile); + switchMontageProfile(nextProfile); + + } + + + } + + //----------------------------------------------------------------------- + // cycle through all displayed monitors and check alarm status + //----------------------------------------------------------------------- + + function loadAlarmStatus() + { + + if ((NVRDataModel.versionCompare($rootScope.apiVersion, "1.30") == -1) || + (NVRDataModel.getBandwidth() == 'lowbw') || + (NVRDataModel.getLogin().disableAlarmCheckMontage == true)) + { + + return; + } + + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + if (($scope.MontageMonitors[i].Monitor.Function == 'None') || + ($scope.MontageMonitors[i].Monitor.Enabled == '0') || + ($scope.MontageMonitors[i].Monitor.listDisplay == 'noshow')) + { + continue; + } + getAlarmStatus($scope.MontageMonitors[i]); + + } + + } + + //----------------------------------------------------------------------- + // get alarm status over HTTP for a single monitor + //----------------------------------------------------------------------- + function getAlarmStatus(monitor) + { + var apiurl = NVRDataModel.getLogin().apiurl; + //console.log ("ALARM CALLED WITH " +JSON.stringify(monitor)); + + var alarmurl = apiurl + "/monitors/alarm/id:" + monitor.Monitor.Id + "/command:status.json"; + // console.log("Alarm Check: Invoking " + alarmurl); + + $http.get(alarmurl) + .then(function(data) + { + // NVRDataModel.debug ("Success in monitor alarmed status " + JSON.stringify(data)); + + var sid = parseInt(data.data.status); + switch (sid) + { + case 0: // idle + monitor.Monitor.alarmState = 'color:rgba(0,0,0,0);'; + break; + case 1: // pre alarm + monitor.Monitor.alarmState = 'color:#e67e22;'; + break; + case 2: // alarm + monitor.Monitor.alarmState = 'color:#D91E18;'; + break; + case 3: // alert + monitor.Monitor.alarmState = 'color:#e67e22;'; + break; + case 4: + monitor.Monitor.alarmState = 'color:#26A65B;'; + break; + + } + + }, + function(error) + { + + monitor.Monitor.alarmState = 'color:rgba(0,0,0,0);'; + NVRDataModel.debug("Error in monitor alarmed status "); + }); + } + + function randEachTime() { + $scope.randToAvoidCacheMem = new Date().getTime(); + //$scope.randToAvoidCacheMem = "1"; + //console.log ("Generating:"+$scope.randToAvoidCacheMem); + } + + //----------------------------------------------------------------------- + // re-compute rand so snapshot in montage reloads + //----------------------------------------------------------------------- + + function loadNotifications() + { + + randEachTime(); + //console.log ($scope.randToAvoidCacheMem); + + if ($scope.areImagesLoading) + { + NVRDataModel.debug("skipping image refresh, packery is still loading"); + return; + } + + //if (pckry && !$scope.isDragabillyOn) pckry.shiftLayout(); + $rootScope.rand = Math.floor((Math.random() * 100000) + 1); + + // if you see the time move, montage should move + + if ($scope.iconTimeNow == 'local') + $scope.timeNow = moment().format(NVRDataModel.getTimeFormatSec()); + else + $scope.timeNow = moment().tz(NVRDataModel.getTimeZoneNow()).format(NVRDataModel.getTimeFormatSec()); + //$scope.timeNow = moment().format(NVRDataModel.getTimeFormatSec()); + + //console.log ("Inside Montage timer..."); + + } + + $scope.cancelReorder = function() + { + $scope.modal.remove(); + }; + + $scope.saveReorder = function() + { + NVRDataModel.debug("Saving monitor hide/unhide"); + + // redo packery as monitor status has changed + // DOM may need reloading if you've hidden/unhidden stuff + $scope.MontageMonitors = $scope.copyMontage; + $scope.modal.remove(); + + $timeout(function() + { + + draggies.forEach(function(drag) + { + drag.destroy(); + }); + + pckry.reloadItems(); + draggies = []; + pckry.once('layoutComplete', savePackeryOrder); + pckry.layout(); + + }, 400); + + }; + + function savePackeryOrder() + { + $timeout(function() + { + var positions = pckry.getShiftPositions('data-item-id'); + NVRDataModel.debug("POSITIONS MAP " + JSON.stringify(positions)); + var ld = NVRDataModel.getLogin(); + ld.packeryPositions = JSON.stringify(positions); + //console.log ("Savtogging " + ld.packeryPositions); + ld.currentMontageProfile = ""; + $scope.currentProfileName = $translate.instant ('kMontage'); + NVRDataModel.setLogin(ld); + + pckry.getItemElements().forEach(function(itemElem) + { + draggie = new Draggabilly(itemElem); + pckry.bindDraggabillyEvents(draggie); + draggies.push(draggie); + draggie.disable(); + }); + + $ionicScrollDelegate.$getByHandle("montage-delegate").scrollTop(); + + // Now also ask DataModel to update its monitor display status + NVRDataModel.reloadMonitorDisplayStatus(); + //$scope.MontageMonitors = angular.copy(NVRDataModel.getMonitorsNow()); + //$scope.MontageMonitors = NVRDataModel.getMonitorsNow(); + pckry.layout(); + }, 20); + } + + $scope.getCycleStatus = function() + { + var c = NVRDataModel.getLogin().cycleMontageProfiles; + var str = (c) ? $translate.instant('kOn'):$translate.instant('kOff'); + return str; + }; + + $scope.toggleCycle = function() + { + var ld = NVRDataModel.getLogin(); + ld.cycleMontageProfiles = !ld.cycleMontageProfiles; + NVRDataModel.setLogin(ld); + NVRDataModel.debug ("cycle="+ld.cycleMontageProfiles); + NVRDataModel.debug ("cycle interval="+ld.cycleMontageInterval); + + + }; + + $scope.toggleHide = function(i) + { + + if ($scope.copyMontage[i].Monitor.listDisplay == 'show') + $scope.copyMontage[i].Monitor.listDisplay = 'noshow'; + else + $scope.copyMontage[i].Monitor.listDisplay = 'show'; + + NVRDataModel.debug("index " + i + " is now " + $scope.copyMontage[i].Monitor.listDisplay); + }; + + $scope.hideUnhide = function() + { + if ($scope.isDragabillyOn) + { + dragToggle(); + } + // make a copy of the current list and work on that + // this is to avoid packery screw ups while you are hiding/unhiding + $scope.copyMontage = angular.copy($scope.MontageMonitors); + $ionicModal.fromTemplateUrl('templates/reorder-modal.html', + { + scope: $scope, + animation: 'slide-in-up', + id:'reorder', + }) + .then(function(modal) + { + $scope.modal = modal; + $scope.reOrderActive = true; + $scope.modal.show(); + }); + }; + + + $scope.$on('modal.removed', function(e, m) + { + + if (m.id != 'reorder') + return; + $scope.reOrderActive = false; + + //console.log ("************** FOOTAGE CLOSED"); + + }); + + /* + $scope.closeReorderModal = function () { + + $scope.modal.remove(); + + }; + */ + + //---------------------------------------------------------------- + // Alarm emit handling + //---------------------------------------------------------------- + $rootScope.$on("alarm", function(event, args) + { + // FIXME: I should probably unregister this instead + if (typeof $scope.monitors === undefined) + return; + //console.log ("***EVENT TRAP***"); + var alarmMonitors = args.message; + for (var i = 0; i < alarmMonitors.length; i++) + { + //console.log ("**** TRAPPED EVENT: "+alarmMonitors[i]); + + for (var j = 0; j < $scope.MontageMonitors.length; j++) + { + if ($scope.MontageMonitors[j].Monitor.Id == alarmMonitors[i]) + { + NVRDataModel.debug("Enabling alarm for Monitor:" + $scope.monitors[j].Monitor.Id); + $scope.MontageMonitors[j].Monitor.isAlarmed = true; + scheduleRemoveFlash(j); + } + } + + } + + }); + + function scheduleRemoveFlash(id) + { + NVRDataModel.debug("Scheduled a " + zm.alarmFlashTimer + "ms timer for dis-alarming monitor ID:" + $scope.MontageMonitors[id].Monitor.Id); + $timeout(function() + { + $scope.MontageMonitors[id].Monitor.isAlarmed = false; + NVRDataModel.debug("dis-alarming monitor ID:" + $scope.MontageMonitors[id].Monitor.Id); + }, zm.alarmFlashTimer); + } + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + $scope.handleAlarmsWhileMinimized = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + + $scope.minimal = !$scope.minimal; + NVRDataModel.debug("MontageCtrl: switch minimal is " + $scope.minimal); + ionic.Platform.fullScreen($scope.minimal, !$scope.minimal); + //console.log ("alarms:Cancelling timer"); + $interval.cancel(intervalHandleMontage); + $interval.cancel(intervalHandleMontageCycle); + $interval.cancel(intervalHandleAlarmStatus); + + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + //------------------------------------------------------------- + // this is checked to make sure we are not pulling images + // when app is in background. This is a problem with Android, + // for example + //------------------------------------------------------------- + + $scope.isBackground = function() + { + //console.log ("Is background called from Montage and returned " + + //NVRDataModel.isBackground()); + return NVRDataModel.isBackground(); + }; + + //--------------------------------------------------------------------- + // Triggered when you enter/exit full screen + //--------------------------------------------------------------------- + $scope.switchMinimal = function() + { + $scope.minimal = !$scope.minimal; + NVRDataModel.debug("MontageCtrl: switch minimal is " + $scope.minimal); + // console.log("Hide Statusbar"); + ionic.Platform.fullScreen($scope.minimal, !$scope.minimal); + //console.log ("minimal switch:Cancelling timer"); + $interval.cancel(intervalHandleMontage); //we will renew on reload + $interval.cancel(intervalHandleMontageCycle); + $interval.cancel(intervalHandleAlarmStatus); + // We are reloading this view, so we don't want entry animations + $ionicHistory.nextViewOptions( + { + disableAnimate: true, + disableBack: true + }); + $state.go("montage", + { + minimal: $scope.minimal, + isRefresh: true + }); + return; + }; + + //--------------------------------------------------------------------- + // Show/Hide PTZ control in monitor view + //--------------------------------------------------------------------- + $scope.togglePTZ = function() + { + $scope.showPTZ = !$scope.showPTZ; + }; + + + function getIndex (mid) + { + var ndx = 0; + for (var i=0; i< $scope.MontageMonitors.length; i++) + { + if ($scope.MontageMonitors[i].Monitor.Id == mid) + { + ndx = i; + break; + } + } + return ndx; + + } + + $scope.toggleStamp = function () + { + if (!$scope.isDragabillyOn) return; + var found = false; + + + for (var i=0; i< $scope.MontageMonitors.length; i++) + { + if ($scope.MontageMonitors[i].Monitor.selectStyle == 'dragborder-selected') + { + + findPackeryElement(i); + } + } + + function findPackeryElement(i) + { + pckry.getItemElements().forEach(function(elem) + { + + var id = elem.getAttribute("data-item-id"); + if (id == $scope.MontageMonitors[i].Monitor.Id) + { + if ($scope.MontageMonitors[i].Monitor.isStamp) + pckry.unstamp(elem); + else + pckry.stamp(elem); + + $scope.MontageMonitors[i].Monitor.isStamp = !$scope.MontageMonitors[i].Monitor.isStamp; + NVRDataModel.debug ("Stamp for "+$scope.MontageMonitors[i].Monitor.Name + " is:"+$scope.MontageMonitors[i].Monitor.isStamp ); + //break; + + } + }); + } + + + }; + + $scope.hideMonitor = function (mid) + { + if (!$scope.isDragabillyOn) return; + var found = false; + for (var i=0; i< $scope.MontageMonitors.length; i++) + { + if ($scope.MontageMonitors[i].Monitor.selectStyle == 'dragborder-selected') + { + $scope.MontageMonitors[i].Monitor.listDisplay = 'noshow'; + $scope.MontageMonitors[i].Monitor.selectStyle = ""; + found = true; + } + + } + if (found) + { + pckry.once ('layoutComplete', saveUpdatedLayout); + $timeout (function() {pckry.shiftLayout();},300); + } + + function saveUpdatedLayout() + { + $timeout(function() + { + var positions = pckry.getShiftPositions('data-item-id'); + console.log("SAVING"); + var ld = NVRDataModel.getLogin(); + + ld.packeryPositions = JSON.stringify(positions); + //console.log ("Saving " + ld.packeryPositions); + ld.currentMontageProfile = ""; + $scope.currentProfileName = $translate.instant ('kMontage'); + NVRDataModel.setLogin(ld); + $ionicLoading.hide(); + //$scope.sliderChanging = false; + }, 20); + } + + }; + + $scope.toggleSelectItem = function(mid) + { + var ndx = getIndex(mid); + //console.log ("TOGGLE DETECTED AT INDEX:"+ndx+" NAME="+$scope.MontageMonitors[ndx].Monitor.Name); + if ($scope.MontageMonitors[ndx].Monitor.selectStyle !== "undefined" && $scope.MontageMonitors[ndx].Monitor.selectStyle == "dragborder-selected") + { + $scope.MontageMonitors[ndx].Monitor.selectStyle = ""; + } + else + { + $scope.MontageMonitors[ndx].Monitor.selectStyle = "dragborder-selected"; + } + //console.log ("Switched value to " + $scope.MontageMonitors[ndx].Monitor.selectStyle); + }; + + //--------------------------------------------------------------------- + // Called when you enable/disable dragging + //--------------------------------------------------------------------- + + $scope.dragToggle = function() + { + dragToggle(); + + }; + + function dragToggle() + { + var i; + $scope.isDragabillyOn = !$scope.isDragabillyOn; + + for ( i = 0; i < $scope.MontageMonitors.length; i++) + { + $scope.MontageMonitors[i].Monitor.isStamp = false; + } + + $ionicSideMenuDelegate.canDragContent($scope.isDragabillyOn ? false : true); + + //$timeout(function(){pckry.reloadItems();},10); + NVRDataModel.debug("setting dragabilly to " + $scope.isDragabillyOn); + if ($scope.isDragabillyOn) + { + $scope.toggleSubMenu = true; + + $scope.dragBorder = "dragborder"; + NVRDataModel.debug("Enabling drag for " + draggies.length + " items"); + for (i = 0; i < draggies.length; i++) + { + draggies[i].enable(); + draggies[i].bindHandles(); + } + + // reflow and reload as some may be hidden + // $timeout(function(){pckry.reloadItems();$timeout(function(){pckry.layout();},300);},100); + } + else + { + $scope.dragBorder = ""; + NVRDataModel.debug("Disabling drag for " + draggies.length + " items"); + for (i = 0; i < draggies.length; i++) + { + draggies[i].disable(); + draggies[i].unbindHandles(); + } + for (i = 0; i < $scope.MontageMonitors.length; i++) + { + $scope.MontageMonitors[i].Monitor.selectStyle = ""; + } + // reflow and reload as some may be hidden + $timeout(function() + { + $timeout(function() + { + var positions = pckry.getShiftPositions('data-item-id'); + //console.log ("POSITIONS MAP " + JSON.stringify(positions)); + var ld = NVRDataModel.getLogin(); + ld.packeryPositions = JSON.stringify(positions); + //console.log ("Saving " + ld.packeryPositions); + ld.currentMontageProfile = ""; + $scope.currentProfileName =$translate.instant ('kMontage'); + NVRDataModel.setLogin(ld); + }, 300); + }, 100); + + } + } + + //--------------------------------------------------------------------- + // main monitor modal open - if drag is not on, this is called on touch + //--------------------------------------------------------------------- + + $scope.openModal = function(mid, controllable, controlid, connKey, monitor) + { + openModal(mid, controllable, controlid, connKey, monitor); + }; + + function openModal(mid, controllable, controlid, connKey, monitor) + { + NVRDataModel.debug("MontageCtrl: Open Monitor Modal with monitor Id=" + mid + " and Controllable:" + controllable + " with control ID:" + controlid); + // $scope.isModalActive = true; + // Note: no need to setAwake(true) as its already awake + // in montage view + + NVRDataModel.log("Cancelling montage timer, opening Modal"); + // NVRDataModel.log("Starting Modal timer"); + //console.log ("openModal:Cancelling timer"); + $interval.cancel(intervalHandleMontage); + $interval.cancel(intervalHandleMontageCycle); + $interval.cancel(intervalHandleAlarmStatus); + + $scope.monitor = monitor; + $scope.showPTZ = false; + $scope.monitorId = mid; + $scope.monitorName = NVRDataModel.getMonitorName(mid); + $scope.controlid = controlid; + + //$scope.LoginData = NVRDataModel.getLogin(); + $rootScope.modalRand = Math.floor(Math.random() * (999999 - 111111 + 1)) + 111111; + + $scope.ptzMoveCommand = ""; + $scope.ptzStopCommand = ""; + + $scope.zoomInCommand = ""; + $scope.zoomOutCommand = ""; + $scope.zoomStopCommand = "zoomStop"; + $scope.canZoom = false; + + $scope.presetOn = false; + + $scope.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + $scope.isControllable = controllable; + $scope.refMonitor = monitor; + + // This is a modal to show the monitor footage + // We need to switch to always awake if set so the feed doesn't get interrupted + NVRDataModel.setAwake(NVRDataModel.getKeepAwake()); + + // This is a modal to show the monitor footage + $ionicModal.fromTemplateUrl('templates/monitors-modal.html', + { + scope: $scope, + animation: 'slide-in-up', + id: 'monitorsmodal' + + }) + .then(function(modal) + { + $scope.modal = modal; + + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait'), + noBackdrop: true, + duration: zm.loadingTimeout + }); + + // we don't really need this as we have stopped the timer + // $scope.isModalActive = true; + + //$timeout (function() {pckry.shiftLayout();},zm.packeryTimer); + $scope.modal.show(); + + }); + + } + + //--------------------------------------------------------------------- + // + //--------------------------------------------------------------------- + + function cleanupOnClose() + { + $scope.modal.remove(); + $timeout(function() + { + NVRDataModel.log("MontageCtrl:Stopping network pull..."); + if (NVRDataModel.isForceNetworkStop()) NVRDataModel.stopNetwork(); + }, 50); + + $rootScope.rand = Math.floor((Math.random() * 100000) + 1); + $scope.isModalActive = false; + + NVRDataModel.log("Restarting montage timer, closing Modal..."); + var ld = NVRDataModel.getLogin(); + // console.log ("closeModal: Cancelling timer"); + $interval.cancel(intervalHandleMontage); + $interval.cancel(intervalHandleAlarmStatus); + $interval.cancel(intervalHandleMontageCycle); + + intervalHandleMontage = $interval(function() + { + loadNotifications(); + // console.log ("Refreshing Image..."); + }.bind(this), refreshSec * 1000); + + intervalHandleAlarmStatus = $interval(function() + { + loadAlarmStatus(); + // console.log ("Refreshing Image..."); + }.bind(this), 5000); + + intervalHandleMontageCycle = $interval(function() + { + cycleMontageProfiles(); + // console.log ("Refreshing Image..."); + }.bind(this), 5000); + + + // $timeout (function() {pckry.shiftLayout();},zm.packeryTimer); + + } + + $scope.closeModal = function() + { + NVRDataModel.debug("MontageCtrl: Close & Destroy Monitor Modal"); + cleanupOnClose(); + // $scope.isModalActive = false; + // Note: no need to setAwake(false) as needs to be awake + // in montage view + + }; + + //--------------------------------------------------------------------- + // In Android, the app runs full steam while in background mode + // while in iOS it gets suspended unless you ask for specific resources + // So while this view, we DON'T want Android to keep sending 1 second + // refreshes to the server for images we are not seeing + //--------------------------------------------------------------------- + + function onPause() + { + NVRDataModel.debug("MontageCtrl: onpause called"); + $interval.cancel(intervalHandleMontage); + $interval.cancel(intervalHandleMontageCycle); + $interval.cancel(intervalHandleAlarmStatus); + // $interval.cancel(modalIntervalHandle); + + // FIXME: Do I need to setAwake(false) here? + } + + function onResume() + { + + } + + $scope.openMenu = function() + { + $timeout(function() + { + $rootScope.stateofSlide = $ionicSideMenuDelegate.isOpen(); + }, 500); + + $ionicSideMenuDelegate.toggleLeft(); + }; + + $scope.$on('$destroy', function() { + + }); + + $scope.$on('$ionicView.loaded', function() + { + // console.log("**VIEW ** Montage Ctrl Loaded"); + }); + + $scope.$on('$ionicView.leave', function() + { + // console.log("**VIEW ** Montage Ctrl Left, force removing modal"); + if ($scope.modal) $scope.modal.remove(); + }); + + function orientationChanged() + { + + } + + + // remove a saved montage profile + $scope.deleteMontageProfile = function() + { + var posArray; + + try + { + posArray = NVRDataModel.getLogin().packeryPositionsArray; + //console.log ("PA="+JSON.stringify(posArray)); + + } + catch (e) + { + NVRDataModel.debug("error parsing packery array positions"); + posArray = {}; + } + + //console.log ("posArray="+JSON.stringify(posArray)); + + $scope.listdata = []; + for (var key in posArray) + { + if (posArray.hasOwnProperty(key)) + { + $scope.listdata.push(key); + } + } + + if (!$scope.listdata.length) + { + + $rootScope.zmPopup = $ionicPopup.alert( + { + title: $translate.instant('kError'), + template: $translate.instant('kMontageNoSavedProfiles'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + return; + } + + $scope.data = { + 'selectedVal': '' + }; + + $rootScope.zmPopup = SecuredPopups.show('confirm', + { + template: '<ion-list> ' + + ' <ion-radio-fix ng-repeat="item in listdata" ng-value="item" ng-model="data.selectedVal"> ' + + ' {{item}} ' + + ' </ion-item> ' + + '</ion-list> ', + + title: $translate.instant('kSelect'), + subTitle:$translate.instant('kSelectDelete'), + scope: $scope, + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + + }).then(function(res) + { + NVRDataModel.debug("Deleting profile: " + $scope.data.selectedVal); + delete posArray[$scope.data.selectedVal]; + var ld = NVRDataModel.getLogin(); + ld.packeryPositionsArray = posArray; + + if (ld.currentMontageProfile == $scope.data.selectedVal) + { + ld.currentMontageProfile = ""; + $scope.currentProfileName = $translate.instant ('kMontage'); + + } + + if ($scope.currentMontageProfile == $scope.data.selectedVal) + $scope.currentProfileName = $translate.instant('kMontage'); + + NVRDataModel.setLogin(ld); + + }); + + }; + + + function switchMontageProfile (mName) + { + $interval.cancel(intervalHandleMontageCycle); + intervalHandleMontageCycle = $interval(function() + { + cycleMontageProfiles(); + // console.log ("Refreshing Image..."); + }.bind(this), NVRDataModel.getLogin().cycleMontageInterval* 1000); + + + //console.log ("SELECTED " + $scope.data.selectedVal); + var ld = NVRDataModel.getLogin(); + //console.log ("OLD POS="+ld.packeryPositions); + ld.packeryPositions = ld.packeryPositionsArray[mName]; + ld.currentMontageProfile = mName; + $scope.currentProfileName =mName; + //console.log ("NEW POS="+ld.packeryPositions); + NVRDataModel.setLogin(ld); + //console.log ("SAVING "+ld.packeryPositions.name+ " but "+$scope.data.selectedVal); + + //$scope.MontageMonitors = angular.copy(NVRDataModel.getMonitorsNow()); + + + draggies.forEach(function(drag) + { + drag.destroy(); + }); + draggies = []; + pckry.destroy(); + NVRDataModel.reloadMonitorDisplayStatus(); + $scope.MontageMonitors = NVRDataModel.getMonitorsNow(); + $timeout (function() {initPackery();},zm.packeryTimer); + + + + } + // switch to another montage profile + $scope.switchMontageProfile = function() + { + var posArray; + + try + { + posArray = NVRDataModel.getLogin().packeryPositionsArray; + //console.log ("PA="+JSON.stringify(posArray)); + + } + catch (e) + { + NVRDataModel.debug("error parsing packery array positions"); + posArray = {}; + } + + //console.log ("posArray="+JSON.stringify(posArray)); + + $scope.listdata = []; + for (var key in posArray) + { + if (posArray.hasOwnProperty(key)) + { + $scope.listdata.push(key); + } + } + if ($scope.listdata.indexOf($translate.instant('kMontageDefaultProfile')) == -1) + $scope.listdata.push($translate.instant('kMontageDefaultProfile')); + + if (!$scope.listdata.length) + { + + $rootScope.zmPopup = $ionicPopup.alert( + { + title: $translate.instant('kError'), + template: $translate.instant('kMontageNoSavedProfiles'), + + }); + return; + } + + $scope.data = { + 'selectedVal': '' + }; + + $rootScope.zmPopup = SecuredPopups.show('confirm', + { + template: '<ion-list> ' + + ' <ion-radio-fix ng-repeat="item in listdata" ng-value="item" ng-model="data.selectedVal"> ' + + ' {{item}} ' + + ' </ion-item> ' + + '</ion-list> ', + + title: $translate.instant('kSelect'), + subTitle:$translate.instant('kSelectSwitch'), + scope: $scope, + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + + + }).then(function(res) + { + if (res) + { + // destroy cycle timer and redo it + // + switchMontageProfile($scope.data.selectedVal); + + + //pckry.reloadItems(); + } + + }); + + }; + + // save current configuration into a profile + $scope.saveMontageProfile = function() + { + + var posArray; + + try + { + posArray = NVRDataModel.getLogin().packeryPositionsArray; + //console.log ("PA="+JSON.stringify(posArray)); + + } + catch (e) + { + NVRDataModel.debug("error parsing packery array positions"); + posArray = {}; + } + $scope.data = { + montageName: "" + }; + + $scope.listdata = []; + for (var key in posArray) + { + if (posArray.hasOwnProperty(key)) + { + $scope.listdata.push(key); + } + } + if ($scope.listdata.indexOf($translate.instant('kMontageDefaultProfile')) == -1) + $scope.listdata.push($translate.instant('kMontageDefaultProfile')); + + + var templ = "<input autocapitalize='none' autocomplete='off' autocorrect='off' type='text' ng-model='data.montageName'>"; + + if ($scope.listdata.length) + templ += '<br/><div class="item item-divider">'+$translate.instant('kMontageSavedProfiles')+'</div>'+ + '<ion-list> ' + + ' <ion-radio-fix ng-repeat="item in listdata" ng-value="item" ng-model="data.montageName"> ' + + ' {{item}} ' + + ' </ion-item> ' + + '</ion-list> '; + + + $rootScope.zmPopup = SecuredPopups.show('confirm', + { + title: $translate.instant('kMontageSave'), + template: templ, + subTitle: $translate.instant('kMontageSaveSubtitle'), + scope: $scope, + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + + }).then(function(res) + { + console.log(res); + if (res) // ok + { + + var ld = NVRDataModel.getLogin(); + + if ($scope.data.montageName != '') + { + // lets allow them to save default + //if ($scope.data.montageName != $translate.instant('kMontageDefaultProfile')) + if (1) + { + var getMonPos = pckry.getShiftPositions('data-item-id'); + var unHidden = false; + + // if you are saving to default all monitor profile + // then I will undo any hidden monitors + if ($scope.data.montageName == $translate.instant('kMontageDefaultProfile')) + { + for ( var p=0; p < getMonPos.length; p++) + { + //console.log ("CHECK"); + if (getMonPos[p].display != 'show') + { + getMonPos[p].display = 'show'; + unHidden = true; + } + } + } + + var pos = JSON.stringify(getMonPos); + + //console.log ("SAVING POS = "+pos); + + ld.packeryPositionsArray[$scope.data.montageName] = pos; + NVRDataModel.debug("Saving " + $scope.data.montageName + " with:" + pos); + ld.currentMontageProfile =$scope.data.montageName ; + NVRDataModel.setLogin(ld); + $scope.currentProfileName = $scope.data.montageName; + + if (unHidden) + { + $rootScope.zmPopup = SecuredPopups.show('alert', + { + title: $translate.instant('kNote'), + template: $translate.instant('kMontageSaveDefaultWarning'), + okText: $translate.instant('kButtonOk'), + + }); + switchMontageProfile($translate.instant('kMontageDefaultProfile')); + + + } + + } + + + } + + } + }); + + }; + + $scope.toggleSubMenuFunction = function() + { + + $scope.toggleSubMenu = !$scope.toggleSubMenu; + + NVRDataModel.debug("toggling size buttons:" + $scope.toggleSubMenu); + if ($scope.toggleSubMenu) $ionicScrollDelegate.$getByHandle("montage-delegate").scrollTop(); + var ld = NVRDataModel.getLogin(); + ld.showMontageSubMenu = $scope.toggleSubMenu; + NVRDataModel.setLogin(ld); + }; + + // minimal has to be beforeEnter or header won't hide + $scope.$on('$ionicView.beforeEnter', function() + { + $scope.minimal = $stateParams.minimal; + //console.log ("**************** MINIMAL ENTER " + $scope.minimal); + $scope.zmMarginTop = $scope.minimal ? 0 : 15; + + }); + + //avoid bogus scale error + $scope.LoginData = NVRDataModel.getLogin(); + + $scope.toggleTimeType = function() + { + if (NVRDataModel.isTzSupported()) + { + if ($scope.iconTimeNow == 'server') + { + $scope.iconTimeNow = 'local'; + $scope.timeNow = $translate.instant('kPleaseWait'); + } + else + { + $scope.iconTimeNow = 'server'; + $scope.timeNow = $translate.instant('kPleaseWait'); + } + } + else + NVRDataModel.debug("timezone API not supported, can't display"); + }; + + $scope.$on('$ionicView.afterEnter', function() + { + NVRDataModel.debug("Setting image mode to snapshot, will change to image when packery is all done"); + $scope.areImagesLoading = true; + $scope.isDragabillyOn = false; + $scope.reOrderActive = false; + + if (NVRDataModel.isTzSupported()) + $scope.iconTimeNow = 'server'; + else + $scope.iconTimeNow = 'local'; + + if ($scope.iconTimeNow == 'local') + $scope.timeNow = moment().format(NVRDataModel.getTimeFormatSec()); + else + $scope.timeNow = moment().tz(NVRDataModel.getTimeZoneNow()).format(NVRDataModel.getTimeFormatSec()); + + $scope.gridScale = "grid-item-50"; + $scope.LoginData = NVRDataModel.getLogin(); + //FIXME + + if (NVRDataModel.getBandwidth() == 'lowbw') + { + NVRDataModel.debug("Enabling low bandwidth parameters"); + $scope.LoginData.montageQuality = zm.montageQualityLowBW; + $scope.LoginData.singleImageQuality = zm.eventSingleImageQualityLowBW; + $scope.LoginData.montageHistoryQuality = zm.montageQualityLowBW; + + } + + $scope.monLimit = $scope.LoginData.maxMontage; + $scope.toggleSubMenu = NVRDataModel.getLogin().showMontageSubMenu; + + $scope.monitors = message; + $scope.MontageMonitors = angular.copy(message); + $scope.sliderChanging = false; + loginData = NVRDataModel.getLogin(); + + $scope.isRefresh = $stateParams.isRefresh; + sizeInProgress = false; + $scope.imageStyle = true; + intervalHandleMontage = ""; + intervalHandleMontageCycle = ""; + $scope.isModalActive = false; + $scope.isReorder = false; + + $ionicSideMenuDelegate.canDragContent($scope.minimal ? true : true); + + $scope.areImagesLoading = true; + var ld = NVRDataModel.getLogin(); + + refreshSec = (NVRDataModel.getBandwidth() == 'lowbw') ? ld.refreshSecLowBW : ld.refreshSec; + + NVRDataModel.debug("bandwidth: " + NVRDataModel.getBandwidth() + " montage refresh set to: " + refreshSec); + + //console.log("Setting Awake to " + NVRDataModel.getKeepAwake()); + NVRDataModel.setAwake(NVRDataModel.getKeepAwake()); + + $interval.cancel(intervalHandleMontage); + $interval.cancel(intervalHandleMontageCycle); + $interval.cancel(intervalHandleAlarmStatus); + + intervalHandleMontage = $interval(function() + { + loadNotifications(); + // console.log ("Refreshing Image..."); + }.bind(this), refreshSec * 1000); + + NVRDataModel.debug ("Setting up cycle interval of:"+ NVRDataModel.getLogin().cycleMontageInterval* 1000); + intervalHandleMontageCycle = $interval(function() + { + cycleMontageProfiles(); + // console.log ("Refreshing Image..."); + }.bind(this), NVRDataModel.getLogin().cycleMontageInterval* 1000); + + intervalHandleAlarmStatus = $interval(function() + { + loadAlarmStatus(); + // console.log ("Refreshing Image..."); + }.bind(this), 5000); + + loadNotifications(); + + if ($scope.MontageMonitors.length == 0) + { + $rootScope.zmPopup = $ionicPopup.alert( + { + title: $translate.instant('kNoMonitors'), + template: $translate.instant('kCheckCredentials'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("login", + { + "wizard": false + }); + return; + } + + ld = NVRDataModel.getLogin(); + + $rootScope.authSession = "undefined"; + $ionicLoading.show( + { + template: $translate.instant('kNegotiatingStreamAuth'), + animation: 'fade-in', + showBackdrop: true, + duration: zm.loadingTimeout, + maxWidth: 300, + showDelay: 0 + }); + + NVRDataModel.log("Inside Montage Ctrl:We found " + $scope.monitors.length + " monitors"); + + // set them all at 50% for packery + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + $scope.MontageMonitors[i].Monitor.gridScale = "50"; + $scope.MontageMonitors[i].Monitor.selectStyle = ""; + $scope.MontageMonitors[i].Monitor.alarmState = 'color:rgba(0,0,0,0);'; + $scope.MontageMonitors[i].Monitor.isStamp = false; + + } + + $rootScope.validMonitorId = $scope.monitors[0].Monitor.Id; + NVRDataModel.getAuthKey($rootScope.validMonitorId, (Math.floor((Math.random() * 999999) + 1)).toString()) + .then(function(success) + { + $ionicLoading.hide(); + //console.log(success); + $rootScope.authSession = success; + NVRDataModel.log("Stream authentication construction: " + + $rootScope.authSession); + $timeout(function() + { + initPackery(); + }, zm.packeryTimer); + + }, + function(error) + { + + $ionicLoading.hide(); + NVRDataModel.debug("MontageCtrl: Error in authkey retrieval " + error); + //$rootScope.authSession=""; + NVRDataModel.log("MontageCtrl: Error returned Stream authentication construction. Retaining old value of: " + $rootScope.authSession); + $timeout(function() + { + initPackery(); + }, zm.packeryTimer); + }); + + //console.log("**VIEW ** Montage Ctrl AFTER ENTER"); + window.addEventListener("resize", orientationChanged, false); + + document.addEventListener("pause", onPause, false); + document.addEventListener("resume", onResume, false); + + }); + + $scope.$on('$ionicView.beforeLeave', function() + { + // console.log("**VIEW ** Montage Ctrl Left, force removing modal"); + + //console.log ("beforeLeave:Cancelling timer"); + $interval.cancel(intervalHandleMontage); + $interval.cancel(intervalHandleMontageCycle); + $interval.cancel(intervalHandleAlarmStatus); + pckry.destroy(); + window.removeEventListener("resize", orientationChanged, false); + + // make sure this is applied in scope digest to stop network pull + // thats why we are doing it beforeLeave + + if (NVRDataModel.isForceNetworkStop()) + { + NVRDataModel.log("MontageCtrl:Stopping network pull..."); + NVRDataModel.stopNetwork(); + + } + + }); + + $scope.$on('$ionicView.unloaded', function() { + + }); + + $scope.resetSizes = function() + { + var somethingReset = false; + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + if ($scope.isDragabillyOn) + { + if ($scope.MontageMonitors[i].Monitor.selectStyle == "dragborder-selected") + { + $scope.MontageMonitors[i].Monitor.gridScale = "50"; + somethingReset = true; + } + } + else + { + $scope.MontageMonitors[i].Monitor.gridScale = "50"; + // somethingReset = true; + } + } + if (!somethingReset && $scope.isDragabillyOn) // nothing was selected + { + for (i = 0; i < $scope.MontageMonitors.length; i++) + { + $scope.MontageMonitors[i].Monitor.gridScale = "50"; + } + } + + $timeout(function() + { + pckry.reloadItems(); + + pckry.once('layoutComplete', function() + { + //console.log ("Layout complete"); + var positions = pckry.getShiftPositions('data-item-id'); + //console.log ("POSITIONS MAP " + JSON.stringify(positions)); + var ld = NVRDataModel.getLogin(); + + ld.packeryPositions = JSON.stringify(positions); + //console.log ("Saving " + ld.packeryPositions); + ld.currentMontageProfile = ""; + $scope.currentProfileName = $translate.instant ('kMontage'); + NVRDataModel.setLogin(ld); + + $timeout(function() + { + NVRDataModel.debug("doing the jiggle and dance..."); + pckry.resize(true); + }, 300); + + // $scope.slider.monsize = 2; + }); + pckry.layout(); + + }, 20); + + }; + + function layout(pckry) + { + pckry.shiftLayout(); + } + + + $scope.squeezeMonitors = function() + { + pckry.once('layoutComplete', resizeComplete); + $timeout (function() {pckry.layout();}); + + function resizeComplete() + { + //console.log ("HERE"); + $timeout(function() + { + var positions = pckry.getShiftPositions('data-item-id'); + console.log("SAVING"); + var ld = NVRDataModel.getLogin(); + + ld.packeryPositions = JSON.stringify(positions); + //console.log ("Saving " + ld.packeryPositions); + ld.currentMontageProfile = ""; + $scope.currentProfileName =$translate.instant ('kMontage'); + NVRDataModel.setLogin(ld); + $ionicLoading.hide(); + $scope.sliderChanging = false; + }, 20); + + } + + + }; + //--------------------------------------------------------- + // slider is tied to the view slider for montage + //Remember not to use a variable. I'm using an object + // so it's passed as a reference - otherwise it makes + // a copy and the value never changes + //--------------------------------------------------------- + + $scope.sliderChanged = function(dirn) + { + + if ($scope.sliderChanging) + { + console.log("too fast my friend"); + //$scope.slider.monsize = oldSliderVal; + return; + } + + $scope.sliderChanging = true; + + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait'), + noBackdrop: true, + duration: 5000 + }); + + var somethingReset = false; + + var oldScales = {}; + pckry.getItemElements().forEach(function(elem) + { + var id = elem.getAttribute("data-item-id"); + var sz = elem.getAttribute("data-item-size"); + if (isNaN(sz)) sz=20; + oldScales[id] = sz; + console.log("REMEMBERING " + id + ":" + sz); + + }); + + // this only changes items that are selected + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + + var curVal = parseInt($scope.MontageMonitors[i].Monitor.gridScale) || 20; + curVal = curVal + (5 * dirn); + if (curVal < 10) curVal = 10; + if (curVal > 100) curVal = 100; + //console.log ("For Index: " + i + " From: " + $scope.MontageMonitors[i].Monitor.gridScale + " To: " + curVal); + + if ($scope.isDragabillyOn) + { + // only do this for selected monitors + if ($scope.MontageMonitors[i].Monitor.selectStyle == "dragborder-selected") + { + + $scope.MontageMonitors[i].Monitor.gridScale = curVal; + somethingReset = true; + } + } + else + { + $scope.MontageMonitors[i].Monitor.gridScale = curVal; + //somethingReset = true; + + } + + } + + // this changes all items if none were selected + if (!somethingReset && $scope.isDragabillyOn) // nothing was selected + { + for (i = 0; i < $scope.MontageMonitors.length; i++) + { + var cv = parseInt($scope.MontageMonitors[i].Monitor.gridScale) || 20; + cv = cv + (5 * dirn); + if (cv < 10) cv = 10; + if (cv > 100) cv = 100; + $scope.MontageMonitors[i].Monitor.gridScale = cv; + //console.log ("*******GRIDSCALE="+) + } + } + + // reload sizes from DOM and trigger a layout + + $timeout(function() + { + console.log("Calling re-layout"); + //pckry.reloadItems(); + + if (dirn == 1) //expand + { + pckry.getItemElements().forEach(function(elem) + { + var id = elem.getAttribute("data-item-id"); + var sz = elem.getAttribute("data-item-size"); + if (isNaN(sz)) sz=20; + console.log("NOW IT IS-> " + id + ":" + sz); + if (oldScales[id] != sz) + { + console.log("Calling FIT on " + id + " size:" + oldScales[id] + "->" + sz); + pckry.once('fitComplete', resizeComplete); + pckry.fit(elem); + + } + }); + } + else //shrink + { + console.log("Calling shift"); + pckry.once('layoutComplete', resizeComplete); + pckry.shiftLayout(); + + } + + }, 20); + + /* if (!somethingReset) { + //console.log (">>>SOMETHING NOT RESET"); + $timeout(function () { + pckry.layout(); + }, zm.packeryTimer); + } else { + + //console.log (">>>SOMETHING RESET"); + $timeout(function () { + layout(pckry); + }, zm.packeryTimer); + }*/ + function resizeComplete() + { + //console.log ("HERE"); + $timeout(function() + { + var positions = pckry.getShiftPositions('data-item-id'); + console.log("SAVING"); + var ld = NVRDataModel.getLogin(); + + ld.packeryPositions = JSON.stringify(positions); + //console.log ("Saving " + ld.packeryPositions); + ld.currentMontageProfile = ""; + $scope.currentProfileName = $translate.instant ('kMontage'); + NVRDataModel.setLogin(ld); + $ionicLoading.hide(); + $scope.sliderChanging = false; + }, 20); + + } + + }; + + $scope.$on('$ionicView.afterEnter', function() + { + // This rand is really used to reload the monitor image in img-src so it is not cached + // I am making sure the image in montage view is always fresh + // I don't think I am using this anymore FIXME: check and delete if needed + // $rootScope.rand = Math.floor((Math.random() * 100000) + 1); + }); + + $scope.currentProfileName = NVRDataModel.getLogin().currentMontageProfile || $translate.instant ('kMontage'); + + $scope.reloadView = function() + { + $rootScope.rand = Math.floor((Math.random() * 100000) + 1); + NVRDataModel.log("User action: image reload " + $rootScope.rand); + }; + + $scope.doRefresh = function() + { + + // console.log("***Pull to Refresh, recomputing Rand"); + NVRDataModel.log("Reloading view for montage view, recomputing rand"); + $rootScope.rand = Math.floor((Math.random() * 100000) + 1); + $scope.monitors = []; + imageLoadingDataShare.set(0); + + var refresh = NVRDataModel.getMonitors(1); + + refresh.then(function(data) + { + $scope.monitors = data; + $scope.$broadcast('scroll.refreshComplete'); + }); + }; + + }]); diff --git a/www/js/MontageHistoryCtrl.js b/www/js/MontageHistoryCtrl.js new file mode 100644 index 00000000..8aff342e --- /dev/null +++ b/www/js/MontageHistoryCtrl.js @@ -0,0 +1,1496 @@ +// Controller for the montage view +/* jshint -W041, -W093, -W083 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console,ionic,Masonry,moment,Packery, Draggabilly, imagesLoaded, Chart */ +// FIXME: This is a copy of montageCtrl - needs a lot of code cleanup +angular.module('zmApp.controllers').controller('zmApp.MontageHistoryCtrl', ['$scope', '$rootScope', 'NVRDataModel', 'message', '$ionicSideMenuDelegate', '$timeout', '$interval', '$ionicModal', '$ionicLoading', '$http', '$state', '$ionicPopup', '$stateParams', '$ionicHistory', '$ionicScrollDelegate', '$ionicPlatform', 'zm', '$ionicPopover', '$controller', 'imageLoadingDataShare', '$window', '$translate', 'qHttp', '$q', function($scope, $rootScope, NVRDataModel, message, $ionicSideMenuDelegate, $timeout, $interval, $ionicModal, $ionicLoading, $http, $state, $ionicPopup, $stateParams, $ionicHistory, $ionicScrollDelegate, $ionicPlatform, zm, $ionicPopover, $controller, imageLoadingDataShare, $window, $translate, qHttp, $q) +{ + //-------------------------------------------------------------------------------------- + // Handles bandwidth change, if required + // + //-------------------------------------------------------------------------------------- + $rootScope.$on("bandwidth-change", function(e, data) + { + // nothing to do for now + // eventUrl will use lower BW in next query cycle + }); + + $scope.getLocalTZ = function() + { + return moment.tz.guess(); + }; + //-------------------------------------- + // formats events dates in a nice way + //--------------------------------------- + $scope.prettifyDateTimeFirst = function(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format(NVRDataModel.getTimeFormat() + '/MMM Do'); + else + return moment(str).format(NVRDataModel.getTimeFormat() + '/MMM Do'); + }; + $scope.prettifyDate = function(str) + { + return moment(str).format('MMM Do, YYYY ' + NVRDataModel.getTimeFormat()); + }; + + function prettifyDate(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format('MMM Do'); + else + return moment(str).format('MMM Do'); + } + $scope.prettifyTime = function(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format('h:mm a'); + else + return moment(str).format('h:mm a'); + }; + $scope.prettify = function(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format(NVRDataModel.getTimeFormat() + ' on MMMM Do YYYY'); + else + return moment(str).format(NVRDataModel.getTimeFormat() + ' on MMMM Do YYYY'); + }; + $scope.humanizeTime = function(str) + { + // if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).fromNow(); + // else + // return moment(str).fromNow(); + + }; + // if you change date in footer, change hrs + $scope.dateChanged = function() + { + $scope.datetimeValueFrom.hrs = Math.round(moment.duration(moment().diff(moment($scope.datetimeValueFrom.value))).asHours()); + }; + // if you change hrs in footer, change date + $scope.hrsChanged = function() + { + $scope.datetimeValueFrom.value = moment().subtract($scope.datetimeValueFrom.hrs, 'hours').toDate(); + timefrom.toDate(); + }; + + function orientationChanged() + { + // NVRDataModel.debug("Detected orientation change, redoing packery resize"); + /* $timeout(function () { + pckry.onresize(); + });*/ + } + //-------------------------------------- + // pause/unpause nph-zms + //--------------------------------------- + $scope.togglePause = function(mid) + { + //console.log ("TOGGLE PAUSE " + mid); + var m = -1; + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + if ($scope.MontageMonitors[i].Monitor.Id == mid) + { + m = i; + break; + } + } + if (m != -1) + { + + $scope.MontageMonitors[m].Monitor.isPaused = !$scope.MontageMonitors[m].Monitor.isPaused; + var cmd = 1; + NVRDataModel.debug("Sending CMD:" + cmd + " for monitor " + $scope.MontageMonitors[m].Monitor.Name); + controlEventStream(cmd, "", $scope.MontageMonitors[m].Monitor.connKey, -1); + } + }; + + function sendCmd(mid, cmd, extra) + { + + var m = -1; + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + if ($scope.MontageMonitors[i].Monitor.Id == mid) + { + m = i; + break; + } + } + if (m != -1) + { + NVRDataModel.debug("Sending CMD:" + cmd + " for monitor " + $scope.MontageMonitors[m].Monitor.Name); + return controlEventStream(cmd, "", $scope.MontageMonitors[m].Monitor.connKey, -1, extra); + } + + } + $scope.seek = function(mid, p) + { + NVRDataModel.debug("Slider called with mid=" + mid + " progress=" + p); + + var m = -1; + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + if ($scope.MontageMonitors[i].Monitor.Id == mid) + { + m = i; + break; + } + } + if (m != -1) + { + $scope.MontageMonitors[i].Monitor.seek = true; + } + + sendCmd(mid, '14', "&offset=" + p) + .then(function(success) + { + //console.log ("Removing seek status from " + $scope.MontageMonitors[i].Monitor.Name); + $scope.MontageMonitors[i].Monitor.seek = false; + + }, + function(err) + { + //console.log ("Removing seek status from " + $scope.MontageMonitors[i].Monitor.Name); + $scope.MontageMonitors[i].Monitor.seek = false; + }); + + }; + $scope.moveFaster = function(mid) + { + sendCmd(mid, 4); + }; + $scope.moveSlower = function(mid) + { + sendCmd(mid, 5); + }; + $scope.movePlay = function(mid) + { + + var m = -1; + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + if ($scope.MontageMonitors[i].Monitor.Id == mid) + { + m = i; + break; + } + } + if (m != -1) + { + $scope.MontageMonitors[m].Monitor.isPaused = false; + var cmd = 2; + NVRDataModel.debug("Sending CMD:" + cmd + " for monitor " + $scope.MontageMonitors[m].Monitor.Name); + controlEventStream(cmd, "", $scope.MontageMonitors[m].Monitor.connKey, -1); + } + }; + //-------------------------------------- + // Called when ion-footer collapses + // note that on init it is also called + //--------------------------------------- + $scope.footerExpand = function() + { + // console.log ("**************** EXPAND CALLED ***************"); + $ionicSideMenuDelegate.canDragContent(false); + }; + $scope.footerCollapse = function() + { + footerCollapse(); + }; + /* Note this is also called when the view is first loaded */ + function footerCollapse() + { + if (readyToRun == false) + { + NVRDataModel.debug("fake call to footerCollapse - ignoring"); + return; + } + + if ($scope.MontageMonitors == undefined) + { + NVRDataModel.debug("montage array is undefined and not ready"); + return; + } + + $interval.cancel($rootScope.eventQueryInterval); + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait'), + noBackdrop: true, + duration: zm.httpTimeout + }); + + $scope.dragBorder = ""; + $scope.isDragabillyOn = false; + $ionicSideMenuDelegate.canDragContent(false); + NVRDataModel.stopNetwork("MontageHistory-footerCollapse"); + var ld = NVRDataModel.getLogin(); + $scope.sliderVal.realRate = $scope.sliderVal.rate * 100; + + var TimeObjectFrom = moment($scope.datetimeValueFrom.value).format("YYYY-MM-DD HH:mm"); + var TimeObjectTo = moment().format('YYYY-MM-DD HH:mm'); + + // At this point of time, we need to ensure From and To are changed to server time + //if (NVRDataModel.getLogin().useLocalTimeZone) + if (1) + { + var localtz = moment.tz.guess(); + var servertz = NVRDataModel.getTimeZoneNow(); + + NVRDataModel.log("Local timezone conversion is on, converting from " + localtz + " to " + servertz); + NVRDataModel.log("Original From: " + TimeObjectFrom + " Original To: " + TimeObjectTo); + + TimeObjectFrom = moment.tz(TimeObjectFrom, localtz).tz(servertz).format("YYYY-MM-DD HH:mm"); + TimeObjectTo = moment.tz(TimeObjectTo, localtz).tz(servertz).format("YYYY-MM-DD HH:mm"); + + NVRDataModel.log("Converted From: " + TimeObjectFrom + " Converted To: " + TimeObjectTo); + + } + + var apiurl; + + // release all active streams + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + $scope.MontageMonitors[i].Monitor.selectStyle = ""; + $scope.MontageMonitors[i].Monitor.eid = "-1"; + // generate new connKeys if timeline changes + if ($scope.MontageMonitors[i].Monitor.eventUrl != 'img/noevent.png') + { + // this means this mid was showing a message, now we need to change it + // so kill prev. stream first + NVRDataModel.log("footerCollapse: Calling kill with " + $scope.MontageMonitors[i].Monitor.connKey + " for Monitor:" + $scope.MontageMonitors[i].Monitor.Name); + //var tmpCK = angular.copy($scope.MontageMonitors[i].Monitor.connKey); + //timedControlEventStream(2500, 17, "", tmpCK, -1); + controlEventStream(17, "", $scope.MontageMonitors[i].Monitor.connKey, -1); + $scope.MontageMonitors[i].Monitor.eventUrl = "img/noevent.png"; + $scope.MontageMonitors[i].Monitor.eid = "-1"; + $scope.MontageMonitors[i].Monitor.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + $scope.MontageMonitors[i].Monitor.noGraph = true; + //console.log ("Generating connkey: " +$scope.MontageMonitors[i].Monitor.connKey); + } + } + // grab events that start on or after the time + apiurl = ld.apiurl + "/events/index/StartTime >=:" + TimeObjectFrom + "/AlarmFrames >=:" + (ld.enableAlarmCount ? ld.minAlarmCount : 0) + ".json"; + NVRDataModel.log("Event timeline API is " + apiurl); + // make sure there are no more than 5 active streams (noevent is ok) + $scope.currentLimit = $scope.monLimit; + //qHttp.get(apiurl) + $http( + { + method: 'get', + url: apiurl + }).then(function(succ) + { + var data = succ.data; + var ld = NVRDataModel.getLogin(); + NVRDataModel.debug("Got " + data.events.length + "new history events..."); + var eid, mid, stime; + for (i = 0; i < data.events.length; i++) + { + mid = data.events[i].Event.MonitorId; + eid = data.events[i].Event.Id; + //console.log ("Event ID:"+eid); + stime = data.events[i].Event.StartTime; + // only take the first one for each monitor + for (var j = 0; j < $scope.MontageMonitors.length; j++) + { + $scope.MontageMonitors[j].Monitor.isPaused = false; + // that's the earliest match and play gapless from there + if ($scope.MontageMonitors[j].Monitor.Id == mid) + { + if ($scope.MontageMonitors[j].Monitor.eventUrl == 'img/noevent.png') + { + // console.log ("Old value of event url " + $scope.MontageMonitors[j].eventUrl); + //console.log ("ldurl is " + ld.streamingurl); + var bw = NVRDataModel.getBandwidth() == "lowbw" ? zm.eventMontageQualityLowBW : ld.montageHistoryQuality; + $scope.MontageMonitors[j].Monitor.eventUrl = ld.streamingurl + "/nph-zms?source=event&mode=jpeg&event=" + eid + "&frame=1&replay=gapless&rate=" + $scope.sliderVal.realRate + "&connkey=" + $scope.MontageMonitors[j].Monitor.connKey + "&scale=" + bw + "&rand=" + $rootScope.rand; + //console.log ("Setting event URL to " +$scope.MontageMonitors[j].Monitor.eventUrl); + // console.log ("SWITCHING TO " + $scope.MontageMonitors[j].eventUrl); + $scope.MontageMonitors[j].Monitor.eventUrlTime = stime; + $scope.MontageMonitors[j].Monitor.eid = eid; + $scope.MontageMonitors[j].Monitor.eventDuration = data.events[i].Event.Length; + $scope.MontageMonitors[j].Monitor.sliderProgress = { + progress: 0 + }; + //console.log(">>> Setting Event for " + $scope.MontageMonitors[j].Monitor.Name + " to " + eid); + // now lets get the API for that event for graphing + $scope.MontageMonitors[j].Monitor.noGraph = true; + + } + } + } + } + // make sure we do our best to get that duration for all monitors + // in the above call, is possible some did not make the cut in the first page + NVRDataModel.log("Making sure all monitors have a fair chance..."); + var promises = []; + for (i = 0; i < $scope.MontageMonitors.length; i++) + { + //console.log("Fair chance check for " + $scope.MontageMonitors[i].Monitor.Name); + if ($scope.MontageMonitors[i].Monitor.eventUrl == 'img/noevent.png') + { + var indivGrab = ld.apiurl + "/events/index/MonitorId:" + $scope.MontageMonitors[i].Monitor.Id + "/StartTime >=:" + TimeObjectFrom + "/AlarmFrames >=:" + (ld.enableAlarmCount ? ld.minAlarmCount : 0) + ".json"; + NVRDataModel.debug("Monitor " + $scope.MontageMonitors[i].Monitor.Id + ":" + $scope.MontageMonitors[i].Monitor.Name + " does not have events, trying " + indivGrab); + var p = getExpandedEvents(i, indivGrab); + promises.push(p); + + } + + } + $q.all(promises).then(doPackery); + + // At this stage, we have both a general events grab, and specific event grabs for MIDS that were empty + + function doPackery() + { + // $ionicLoading.hide(); + //console.log("REDOING PACKERY & DRAG"); + NVRDataModel.debug("Re-creating packery and draggy"); + if (pckry !== undefined) + { + // remove current draggies + draggies.forEach(function(drag) + { + drag.destroy(); + }); + draggies = []; + // destroy existing packery object + pckry.destroy(); + initPackery(); + + $rootScope.eventQueryInterval = $interval(function() + { + checkAllEvents(); + }.bind(this), zm.eventHistoryTimer); + } + } + }, function(err) + { + NVRDataModel.debug("history ERROR:" + JSON.stringify(err)); + }); + + function getExpandedEvents(i, indivGrab) + { + var d = $q.defer(); + var ld = NVRDataModel.getLogin(); + // console.log ("Expanded API: " + indivGrab); + $http( + { + method: 'get', + url: indivGrab + }).then(function(succ) + { + var data = succ.data; + // console.log ("EXPANDED DATA FOR MONITOR " + i + JSON.stringify(data)); + if (data.events.length > 0) + { + if (!NVRDataModel.isBackground()) + { + var bw = NVRDataModel.getBandwidth() == "lowbw" ? zm.eventMontageQualityLowBW : ld.montageHistoryQuality; + $scope.MontageMonitors[i].Monitor.eventUrl = ld.streamingurl + "/nph-zms?source=event&mode=jpeg&event=" + data.events[0].Event.Id + "&frame=1&replay=gapless&rate=" + $scope.sliderVal.realRate + "&connkey=" + $scope.MontageMonitors[i].Monitor.connKey + "&scale=" + bw + "&rand=" + $rootScope.rand; + //console.log ("SWITCHING TO " + $scope.MontageMonitors[i].eventUrl); + $scope.MontageMonitors[i].Monitor.eventUrlTime = data.events[0].Event.StartTime; + $scope.MontageMonitors[i].Monitor.eid = data.events[0].Event.Id; + $scope.MontageMonitors[i].Monitor.noGraph = true; + $scope.MontageMonitors[i].Monitor.sliderProgress = { + progress: 0 + }; + $scope.MontageMonitors[i].Monitor.eventDuration = data.events[0].Event.Length; + //console.log(">>> Setting Event for " + $scope.MontageMonitors[i].Monitor.Name + " to " + data.events[0].Event.Id); + NVRDataModel.log("Found expanded event " + data.events[0].Event.Id + " for monitor " + $scope.MontageMonitors[i].Monitor.Id); + } + else + { + // $scope.MontageMonitors[i].eventUrl="img/noevent.png"; + // $scope.MontageMonitors[i].eventUrlTime = ""; + // NVRDataModel.log ("Setting img src to null as data received in background"); + } + } + d.resolve(true); + return d.promise; + }, + function(err) + { + d.resolve(true); + return d.promise; + } + + ); + return d.promise; + } + } + //--------------------------------------------------------- + // This is periodically called to get the current playing + // event by zms. I use this to display a timestamp + // Its a 2 step process - get event Id then go a Event + // API call to get time stamp. Sucks + //--------------------------------------------------------- + function checkAllEvents() + { + //console.log("Timer:Events are checked...."); + + //if (pckry && !$scope.isDragabillyOn) pckry.shiftLayout(); + + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + // don't check for monitors that are not shown + // because nph connkey won't exist and the response + // will fail + if ($scope.MontageMonitors[i].Monitor.eventUrl != "" && $scope.MontageMonitors[i].Monitor.eventUrl != 'img/noevent.png' && $scope.MontageMonitors[i].Monitor.connKey != '' && $scope.MontageMonitors[i].Monitor.Function != 'None' && $scope.MontageMonitors[i].Monitor.listDisplay != 'noshow' && $scope.MontageMonitors[i].Monitor.Enabled != '0') + { + // NVRDataModel.debug("Checking event status for " + $scope.MontageMonitors[i].Monitor.Name + ":" + $scope.MontageMonitors[i].Monitor.eventUrl + ":" + $scope.MontageMonitors[i].Monitor.Function + ":" + $scope.MontageMonitors[i].Monitor.listDisplay); + // console.log ("Sending query 99 for " + $scope.MontageMonitors[i].Monitor.Name + " with ck="+$scope.MontageMonitors[i].Monitor.connKey); + controlEventStream('99', '', $scope.MontageMonitors[i].Monitor.connKey, i); + } + } + } + //-------------------------------------------------------------- + // Used to control zms for a connkey. If ndx is not -1, + // then it also calls an event API for the returned eid + // and stores its time in the montage monitors array + //-------------------------------------------------------------- + $scope.controlEventStream = function(cmd, disp, connkey, ndx) + { + controlEventStream(cmd, disp, connkey, ndx); + }; + + function timedControlEventStream(mTime, cmd, disp, connkey, ndx) + { + var mMtime = mTime || 2000; + NVRDataModel.debug("Deferring control " + cmd + " by " + mMtime); + $timeout(function() + { + subControlStream(cmd, connkey); + }, mMtime); + } + + function subControlStream(cmd, connkey) + { + var loginData = NVRDataModel.getLogin(); + var myauthtoken = $rootScope.authSession.replace("&auth=", ""); + //&auth= + var req = qHttp( + { + method: 'POST', + /*timeout: 15000,*/ + url: loginData.url + '/index.php', + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', //'Accept': '*/*', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + var foo = str.join("&"); + //console.log("****SUB RETURNING " + foo); + return foo; + }, + data: + { + view: "request", + request: "stream", + connkey: connkey, + command: cmd, + auth: myauthtoken, // user: loginData.username, + // pass: loginData.password + } + }); + req.then(function(succ) + { + NVRDataModel.debug("subControl success:" + JSON.stringify(succ)); + }, function(err) + { + NVRDataModel.debug("subControl error:" + JSON.stringify(err)); + }); + } + + function controlEventStream(cmd, disp, connkey, ndx, extras) + { + // console.log("Command value " + cmd); + + var d = $q.defer(); + if (disp) + { + $ionicLoading.hide(); + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + "...", + noBackdrop: true, + duration: zm.loadingTimeout, + }); + } + var loginData = NVRDataModel.getLogin(); + /* + var CMD_NONE = 0; + var CMD_PAUSE = 1; + var CMD_PLAY = 2; + var CMD_STOP = 3; + var CMD_FASTFWD = 4; + var CMD_SLOWFWD = 5; + var CMD_SLOWREV = 6; + var CMD_FASTREV = 7; + var CMD_ZOOMIN = 8; + var CMD_ZOOMOUT = 9; + var CMD_PAN = 10; + var CMD_SCALE = 11; + var CMD_PREV = 12; + var CMD_NEXT = 13; + var CMD_SEEK = 14; + var CMD_QUERY = 99; + */ + // You need to POST commands to control zms + // Note that I am url encoding the parameters into the URL + // If I leave it as JSON, it gets converted to OPTONS due + // to CORS behaviour and ZM/Apache don't seem to handle it + //console.log("POST: " + loginData.url + '/index.php'); + //console.log ("AUTH IS " + $rootScope.authSession); + var myauthtoken = $rootScope.authSession.replace("&auth=", ""); + //&auth= + var req = qHttp( + { + method: 'POST', + /*timeout: 15000,*/ + url: loginData.url + '/index.php', + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', //'Accept': '*/*', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + var foo = str.join("&"); + if (extras) foo = foo + extras; + //console.log("****RETURNING " + foo); + return foo; + }, + data: + { + view: "request", + request: "stream", + connkey: connkey, + command: cmd, + auth: myauthtoken, // user: loginData.username, + // pass: loginData.password + } + }); + req.then(function(succ) + { + var resp = succ.data; + + //console.log ("zms response: " + JSON.stringify(resp)); + + // move progress bar if event id is the same + if (resp.result == "Ok" && ndx != -1 && (resp.status.event == $scope.MontageMonitors[ndx].Monitor.eid)) + { + if (!$scope.MontageMonitors[ndx].Monitor.seek) + { + $scope.MontageMonitors[ndx].Monitor.sliderProgress.progress = resp.status.progress; + } + else + { + NVRDataModel.debug("Skipping progress as seek is active for " + $scope.MontageMonitors[ndx].Monitor.Name); + } + } + + if (resp.result == "Ok" && ndx != -1 && ((resp.status.event != $scope.MontageMonitors[ndx].Monitor.eid) || $scope.MontageMonitors[ndx].Monitor.noGraph == true)) + { + $scope.MontageMonitors[ndx].Monitor.noGraph = false; + // $scope.MontageMonitors[ndx].Monitor.sliderProgress.progress = 0; + NVRDataModel.debug("Fetching details, as event changed for " + $scope.MontageMonitors[ndx].Monitor.Name + " from " + $scope.MontageMonitors[ndx].Monitor.eid + " to " + resp.status.event); + var ld = NVRDataModel.getLogin(); + var apiurl = ld.apiurl + "/events/" + resp.status.event + ".json"; + //console.log ("API " + apiurl); + qHttp( + { + method: 'get', + url: apiurl + }).then(function(succ) + { + var data = succ.data; + var currentEventTime = moment(data.event.Event.StartTime); + var maxTime = moment(); + //NVRDataModel.debug ("Monitor: " + $scope.MontageMonitors[ndx].Monitor.Id + " max time="+maxTime + "("+$scope.datetimeValueTo.value+")"+ " current="+currentEventTime + "("+data.event.Event.StartTime+")"); + + NVRDataModel.debug("creating graph for " + $scope.MontageMonitors[ndx].Monitor.Name); + var framearray = { + labels: [], + datasets: [ + { + backgroundColor: 'rgba(242, 12, 12, 0.5)', + borderColor: 'rgba(242, 12, 12, 0.5)', + data: [], + }] + }; + framearray.labels = []; + var ld = NVRDataModel.getLogin(); + //console.log(">>>>> GRAPH"); + for (i = 0; i < data.event.Frame.length; i++) + { + var ts = moment(data.event.Frame[i].TimeStamp).format(timeFormat); + //console.log ("pushing s:" + event.Frame[i].Score+" t:"+ts); + framearray.datasets[0].data.push( + { + x: ts, + y: data.event.Frame[i].Score + }); + framearray.labels.push(""); + } + $timeout(function() + { + drawGraph(framearray, $scope.MontageMonitors[ndx].Monitor.Id); + }, 100); + var element = angular.element(document.getElementById($scope.MontageMonitors[ndx].Monitor.Id + "-timeline")); + element.removeClass('animated flipInX'); + element.addClass('animated flipOutX'); + $timeout(function() + { + element.removeClass('animated flipOutX'); + element.addClass('animated flipInX'); + $scope.MontageMonitors[ndx].Monitor.eventUrlTime = data.event.Event.StartTime; + var bw = NVRDataModel.getBandwidth() == "lowbw" ? zm.eventMontageQualityLowBW : ld.montageHistoryQuality; + $scope.MontageMonitors[ndx].Monitor.eventUrl = ld.streamingurl + "/nph-zms?source=event&mode=jpeg&event=" + data.event.Event.Id + "&frame=1&replay=gapless&rate=" + $scope.sliderVal.realRate + "&connkey=" + $scope.MontageMonitors[ndx].Monitor.connKey + "&scale=" + bw + "&rand=" + $rootScope.rand; + $scope.MontageMonitors[ndx].Monitor.eid = data.event.Event.Id; + $scope.MontageMonitors[ndx].Monitor.sliderProgress = { + progress: 0 + }; + $scope.MontageMonitors[ndx].Monitor.eventDuration = data.event.Event.Length; + //console.log(">>> Setting Event for " + $scope.MontageMonitors[ndx].Monitor.Name + " to " + data.event.Event.Id); + }, 700); + + }, function(err) + { + NVRDataModel.debug("skipping graph as detailed API failed for " + $scope.MontageMonitors[ndx].Monitor.Name); + $scope.MontageMonitors[ndx].Monitor.eventUrlTime = "-"; + }); + } + d.resolve(true); + return d.promise; + }, function(err) + { + d.reject(false); + NVRDataModel.log("Error sending event command " + JSON.stringify(err), "error"); + return d.promise; + }); + return d.promise; + } + $scope.isBackground = function() + { + return NVRDataModel.isBackground(); + }; + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + $scope.handleAlarmsWhileMinimized = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + $scope.minimal = !$scope.minimal; + NVRDataModel.debug("MontageHistoryCtrl: switch minimal is " + $scope.minimal); + ionic.Platform.fullScreen($scope.minimal, !$scope.minimal); + $interval.cancel(intervalHandle); + $interval.cancel($rootScope.eventQueryInterval); + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false, + }, + { + reload: true + }); + return; + } + }; + //------------------------------------------------------------- + // this is checked to make sure we are not pulling images + // when app is in background. This is a problem with Android, + // for example + //------------------------------------------------------------- + $scope.isBackground = function() + { + //console.log ("Is background called from Montage and returned " + + //NVRDataModel.isBackground()); + return NVRDataModel.isBackground(); + }; + $scope.toggleControls = function() + { + $scope.showControls = !$scope.showControls; + }; + $scope.toggleSelectItem = function(ndx) + { + if ($scope.MontageMonitors[ndx].Monitor.selectStyle !== "undefined" && $scope.MontageMonitors[ndx].Monitor.selectStyle == "dragborder-selected") + { + $scope.MontageMonitors[ndx].Monitor.selectStyle = ""; + } + else + { + $scope.MontageMonitors[ndx].Monitor.selectStyle = "dragborder-selected"; + } + //console.log ("Switched value to " + $scope.MontageMonitors[ndx].Monitor.selectStyle); + }; + //--------------------------------------------------------------------- + // Called when you enable/disable dragging + //--------------------------------------------------------------------- + $scope.dragToggle = function() + { + dragToggle(); + }; + + function dragToggle() + { + var i; + $scope.isDragabillyOn = !$scope.isDragabillyOn; + $ionicSideMenuDelegate.canDragContent($scope.isDragabillyOn ? false : true); + //$timeout(function(){pckry.reloadItems();},10); + NVRDataModel.debug("setting dragabilly to " + $scope.isDragabillyOn); + if ($scope.isDragabillyOn) + { + $scope.showSizeButtons = true; + $scope.dragBorder = "dragborder"; + NVRDataModel.debug("Enabling drag for " + draggies.length + " items"); + for (i = 0; i < draggies.length; i++) + { + draggies[i].enable(); + draggies[i].bindHandles(); + } + // reflow and reload as some may be hidden + // $timeout(function(){pckry.reloadItems();$timeout(function(){pckry.layout();},300);},100); + } + else + { + $scope.dragBorder = ""; + NVRDataModel.debug("Disabling drag for " + draggies.length + " items"); + for (i = 0; i < draggies.length; i++) + { + draggies[i].disable(); + draggies[i].unbindHandles(); + } + for (i = 0; i < $scope.MontageMonitors.length; i++) + { + $scope.MontageMonitors[i].Monitor.selectStyle = ""; + } + // reflow and reload as some may be hidden + $timeout(function() + { + $timeout(function() + { + pckry.shiftLayout(); + /*var positions = pckry.getShiftPositions('data-item-id'); + //console.log ("POSITIONS MAP " + JSON.stringify(positions)); + var ld = NVRDataModel.getLogin(); + ld.packeryPositions = JSON.stringify(positions); + NVRDataModel.setLogin(ld);*/ + }, 300); + }, 100); + } + } + //--------------------------------------------------------------------- + // Show/Hide PTZ control in monitor view + //--------------------------------------------------------------------- + $scope.togglePTZ = function() + { + $scope.showPTZ = !$scope.showPTZ; + }; + $scope.callback = function() + { + // console.log("dragging"); + }; + $scope.onDropComplete = function(index, obj, event) + { + //console.log("dragged"); + var otherObj = $scope.MontageMonitors[index]; + var otherIndex = $scope.MontageMonitors.indexOf(obj); + $scope.MontageMonitors[index] = obj; + $scope.MontageMonitors[otherIndex] = otherObj; + }; + //--------------------------------------------------------------------- + // changes order of montage display + //--------------------------------------------------------------------- + $scope.toggleMontageDisplayOrder = function() + { + $scope.packMontage = !$scope.packMontage; + loginData.packMontage = $scope.packMontage; + NVRDataModel.setLogin(loginData); + //console.log ("Switching orientation"); + }; + //--------------------------------------------------------------------- + // In Android, the app runs full steam while in background mode + // while in iOS it gets suspended unless you ask for specific resources + // So while this view, we DON'T want Android to keep sending 1 second + // refreshes to the server for images we are not seeing + //--------------------------------------------------------------------- + function onPause() + { + NVRDataModel.debug("MontageHistoryCtrl: onpause called"); + $interval.cancel($rootScope.eventQueryInterval); + $interval.cancel(intervalHandle); + // $interval.cancel(modalIntervalHandle); + // FIXME: Do I need to setAwake(false) here? + } + + function onResume() + {} + $scope.openMenu = function() + { + $timeout(function() + { + $rootScope.stateofSlide = $ionicSideMenuDelegate.isOpen(); + }, 500); + $ionicSideMenuDelegate.toggleLeft(); + }; + $scope.$on('$destroy', function() + { + NVRDataModel.debug("Cancelling eventQueryInterval"); + $interval.cancel($rootScope.eventQueryInterval); + }); + $scope.$on('$ionicView.loaded', function() + { + //console.log("**VIEW ** MontageHistoryCtrl Loaded"); + }); + $scope.$on('$ionicView.enter', function() + { + NVRDataModel.debug("**VIEW ** MontageHistory Ctrl Entered"); + var ld = NVRDataModel.getLogin(); + //console.log("Setting Awake to " + NVRDataModel.getKeepAwake()); + NVRDataModel.setAwake(NVRDataModel.getKeepAwake()); + NVRDataModel.debug("query timer started"); + $interval.cancel($rootScope.eventQueryInterval); + //console.log ("****************** TIMER STARTED INSIDE ENTER"); + $rootScope.eventQueryInterval = $interval(function() + { + checkAllEvents(); + }.bind(this), zm.eventHistoryTimer); + }); + /*$scope.$on ('$ionicView.unloaded', function() { + console.log ("******** HISTORY UNLOADED KILLING WINDOW ************"); + window.stop(); + });*/ + $scope.$on('$ionicView.beforeEnter', function() + { + // NVRDataModel.log ("Before Enter History: initing connkeys"); + }); + $scope.$on('$ionicView.beforeLeave', function() + { + //console.log("**VIEW ** Event History Ctrl Left, force removing modal"); + if ($scope.modal) $scope.modal.remove(); + NVRDataModel.log("BeforeLeave: Nullifying the streams..."); + for (i = 0; i < $scope.MontageMonitors.length; i++) + { + var element = document.getElementById("img-" + i); + /*if (element) + { + NVRDataModel.debug("BeforeLeave: Nullifying " + element.src); + element.src=""; + //element.removeAttribute('src'); + + //$scope.$apply(nullify(element)); + //element.src=""; + }*/ + } + NVRDataModel.log("Cancelling event query timer"); + $interval.cancel($rootScope.eventQueryInterval); + NVRDataModel.log("MontageHistory:Stopping network pull..."); + // make sure this is applied in scope digest to stop network pull + // thats why we are doing it beforeLeave + for (i = 0; i < $scope.MontageMonitors.length; i++) + { + if ($scope.MontageMonitors[i].Monitor.connKey != '' && $scope.MontageMonitors[i].Monitor.eventUrl != 'img/noevent.png' && $scope.MontageMonitors[i].Monitor.Function != 'None' && $scope.MontageMonitors[i].Monitor.lisDisplay != 'noshow' && $scope.MontageMonitors[i].Monitor.Enabled != '0') + { + NVRDataModel.log("Before leave: Calling kill with " + $scope.MontageMonitors[i].Monitor.connKey); + var tmpCK = angular.copy($scope.MontageMonitors[i].Monitor.connKey); + timedControlEventStream(2500, 17, "", tmpCK, -1); + } + } + pckry.destroy(); + window.removeEventListener("resize", orientationChanged, false); + NVRDataModel.log("Forcing a window.stop() here"); + NVRDataModel.stopNetwork("MontageHistory-beforeLeave"); + }); + $scope.$on('$ionicView.unloaded', function() {}); + $scope.sliderChanged = function(dirn) + { + //console.log("SLIDER CHANGED"); + if ($scope.sliderChanging) + { + // console.log ("too fast my friend"); + //$scope.slider.monsize = oldSliderVal; + // return; + } + $scope.sliderChanging = true; + var somethingReset = false; + // this only changes items that are selected + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + var curVal = parseInt($scope.MontageMonitors[i].Monitor.gridScale); + curVal = curVal + (10 * dirn); + if (curVal < 10) curVal = 10; + if (curVal > 100) curVal = 100; + //console.log ("For Index: " + i + " From: " + $scope.MontageMonitors[i].Monitor.gridScale + " To: " + curVal); + if ($scope.isDragabillyOn) + { + // only do this for selected monitors + if ($scope.MontageMonitors[i].Monitor.selectStyle == "dragborder-selected") + { + $scope.MontageMonitors[i].Monitor.gridScale = curVal; + somethingReset = true; + } + } + else + { + $scope.MontageMonitors[i].Monitor.gridScale = curVal; + //somethingReset = true; + } + } + // this changes all items if none were selected + if (!somethingReset && $scope.isDragabillyOn) // nothing was selected + { + for (i = 0; i < $scope.MontageMonitors.length; i++) + { + var cv = parseInt($scope.MontageMonitors[i].Monitor.gridScale); + cv = cv + (10 * dirn); + if (cv < 10) cv = 10; + if (cv > 100) cv = 100; + $scope.MontageMonitors[i].Monitor.gridScale = cv; + } + } + //pckry.reloadItems(); + pckry.once('layoutComplete', function() + { + /* $timeout(function () { + var positions = pckry.EHgetShiftPositions('eh-data-item-id'); + //console.log ("POSITIONS MAP " + JSON.stringify(positions)); + var ld = NVRDataModel.getLogin(); + ld.EHpackeryPositions = JSON.stringify(positions); + NVRDataModel.setLogin(ld); + $ionicLoading.hide(); + $scope.sliderChanging = false; + }, zm.packeryTimer);*/ + }); + if (!somethingReset) + { + //console.log (">>>SOMETHING NOT RESET"); + $timeout(function() + { + pckry.layout(); + }, zm.packeryTimer); + } + else + { + //console.log (">>>SOMETHING RESET"); + $timeout(function() + { + layout(pckry); + }, zm.packeryTimer); + } + }; + + function layout(pckry) + { + pckry.shiftLayout(); + } + $scope.resetSizes = function() + { + var somethingReset = false; + for (var i = 0; i < $scope.MontageMonitors.length; i++) + { + if ($scope.isDragabillyOn) + { + if ($scope.MontageMonitors[i].Monitor.selectStyle == "dragborder-selected") + { + $scope.MontageMonitors[i].Monitor.gridScale = "50"; + somethingReset = true; + } + } + else + { + $scope.MontageMonitors[i].Monitor.gridScale = "50"; + // somethingReset = true; + } + } + if (!somethingReset && $scope.isDragabillyOn) // nothing was selected + { + for (i = 0; i < $scope.MontageMonitors.length; i++) + { + $scope.MontageMonitors[i].Monitor.gridScale = "50"; + } + } + $timeout(function() + { + pckry.reloadItems(); + $timeout(function() + { + pckry.layout(); + }, zm.packeryTimer); // force here - no shiftlayout + }, 100); + }; + + function isEmpty(obj) + { + for (var prop in obj) + { + return false; + } + return true; + } + // called by afterEnter to load Packery + function initPackery() + { + $ionicLoading.show( + { + template: $translate.instant('kArrangingImages'), + noBackdrop: true, + duration: zm.loadingTimeout + }); + var progressCalled = false; + draggies = []; + var layouttype = true; + var ld = NVRDataModel.getLogin(); + + var elem = angular.element(document.getElementById("mygrid")); + pckry = new Packery('.grid', + { + itemSelector: '.grid-item', + percentPosition: true, + columnWidth: '.grid-sizer', + gutter: 0, + initLayout: true + + }); + //console.log ("**** mygrid is " + JSON.stringify(elem)); + imagesLoaded(elem).on('progress', function(instance, img) + { + var result = img.isLoaded ? 'loaded' : 'broken'; + NVRDataModel.debug('~~loaded image is ' + result + ' for ' + img.img.src); + pckry.layout(); + progressCalled = true; + // if (layouttype) $timeout (function(){layout(pckry);},100); + }); + imagesLoaded(elem).once('always', function() + { + //console.log("******** ALL IMAGES LOADED"); + $scope.$digest(); + NVRDataModel.debug("All images loaded"); + $ionicLoading.hide(); + + $scope.areImagesLoading = false; + + if (!progressCalled) + { + NVRDataModel.log("*** PROGRESS WAS NOT CALLED"); + pckry.reloadItems(); + } + + $timeout(function() + { + + pckry.getItemElements().forEach(function(itemElem) + { + draggie = new Draggabilly(itemElem); + pckry.bindDraggabillyEvents(draggie); + draggies.push(draggie); + draggie.disable(); + draggie.unbindHandles(); + }); + + pckry.on('dragItemPositioned', itemDragged); + + /*if (!isEmpty(positions)) { + NVRDataModel.log("Arranging as per packery grid"); + + for (var i = 0; i < $scope.MontageMonitors.length; i++) { + for (var j = 0; j < positions.length; j++) { + if ($scope.MontageMonitors[i].Monitor.Id == positions[j].attr) { + $scope.MontageMonitors[i].Monitor.gridScale = positions[j].size; + $scope.MontageMonitors[i].Monitor.listDisplay = positions[j].display; + NVRDataModel.debug("Setting monitor ID: " + $scope.MontageMonitors[i].Monitor.Id + " to size: " + positions[j].size + " and display:" + positions[j].display); + } + //console.log ("Index:"+positions[j].attr+ " with size: " + positions[j].size); + } + } + + + NVRDataModel.debug("All images loaded, doing image layout"); + $timeout(function () { + pckry.initShiftLayout(positions, 'data-item-id'); + }, 0); + }*/ + + $timeout(function() + { + NVRDataModel.log("Force calling resize"); + pckry.layout(); + $scope.packeryDone = true; + }, zm.packeryTimer); // don't ask + + }, zm.packeryTimer); + + }); + + function itemDragged(item) + { + NVRDataModel.debug("drag complete"); + } + } + $scope.$on('$ionicView.beforeEnter', function() + { + // This rand is really used to reload the monitor image in img-src so it is not cached + // I am making sure the image in montage view is always fresh + // I don't think I am using this anymore FIXME: check and delete if needed + // $rootScope.rand = Math.floor((Math.random() * 100000) + 1); + $scope.showControls = true; + $scope.packeryDone = false; + readyToRun = false; + $scope.MontageMonitors = message; + + doInitCode(); + + }); + $scope.reloadView = function() + { + $rootScope.rand = Math.floor((Math.random() * 100000) + 1); + NVRDataModel.log("User action: image reload " + $rootScope.rand); + }; + $scope.doRefresh = function() + { + //console.log("***Pull to Refresh, recomputing Rand"); + NVRDataModel.log("Reloading view for montage view, recomputing rand"); + $rootScope.rand = Math.floor((Math.random() * 100000) + 1); + $scope.MontageMonitors = []; + imageLoadingDataShare.set(0); + var refresh = NVRDataModel.getMonitors(1); + refresh.then(function(data) + { + $scope.MontageMonitors = data.data; + $scope.$broadcast('scroll.refreshComplete'); + }); + }; + + function drawGraph(f, mid) + { + //console.log("Graphing on " + "eventchart-" + mid); + var cv = document.getElementById("eventchart-" + mid); + var ctx = cv.getContext("2d"); + frameoptions = { + responsive: true, + legend: false, + title: + { + display: false, + text: "" + }, + scales: + { + yAxes: [ + { + display: false, + scaleLabel: + { + display: false, + labelString: 'value', + } + }], + xAxes: [ + { + type: 'time', + display: false, + time: + { + format: timeFormat, + tooltipFormat: 'll HH:mm', + min: f.datasets[0].data[0].x, + max: f.datasets[0].data[f.datasets[0].data.length - 1].x, + displayFormats: + {} + }, + scaleLabel: + { + display: false, + labelString: '' + } + }] + } + }; + $timeout(function() + { + var myChart = new Chart(ctx, + { + type: 'line', + data: f, + options: frameoptions, + }); + }); + } + //--------------------------------------------------------------------- + // Controller main + //--------------------------------------------------------------------- + var intervalHandle; + var modalIntervalHandle; + var timeFormat; + var curYear; + var readyToRun; + var frameoptions; + var timeto, timefrom; + var commonCss; + var sizeInProgress; + var ld; + var pckry; + var draggies; + var i; + var draggie; + var loginData; + var oldmonitors; + var gridcontainer; + var montageOrder, hiddenOrder; + + $scope.sliderVal = { + rate: 2, + realRate: 200, + hideNoEvents: false, + enableGapless: true, + exactMatch: false, + showTimeline: true + }; + $scope.timeFormat = "yyyy-MM-dd " + NVRDataModel.getTimeFormat(); + $scope.displayDateTimeSliders = true; + $scope.showtimers = true; + $scope.loginData = NVRDataModel.getLogin(); + + $scope.slider_modal_options_rate = { + from: 1, + to: 10, + realtime: true, + step: 1, + className: "mySliderClass", //modelLabels:function(val) {return "";}, + smooth: false, + css: commonCss, + dimension: 'X' + }; + + $scope.datetimeValueFrom = { + value: "", + hrs: "" + }; + $scope.datetimeValueTo = { + value: "" + }; + + $rootScope.eventQueryInterval = ""; + + function doInitCode() + + { + + $scope.isModalActive = false; + + $scope.hrsAgo = 4; + window.addEventListener("resize", orientationChanged, false); + document.addEventListener("pause", onPause, false); + document.addEventListener("resume", onResume, false); + + timeFormat = 'MM/DD/YYYY HH:mm:ss'; + curYear = new Date().getFullYear(); + readyToRun = false; + + frameoptions = []; + // default = start of day + timeto = moment(); + timefrom = moment().startOf('day'); + $scope.datetimeValueTo.value = timeto.toDate(); + $scope.sliderVal.rate = 1; + $scope.sliderVal.realRate = $scope.sliderVal.rate * 100; + + $scope.datetimeValueFrom.value = timefrom.toDate(); + $scope.datetimeValueFrom.hrs = Math.round(moment.duration(moment().diff(moment($scope.datetimeValueFrom.value))).asHours()); + + commonCss = { + background: + { + "background-color": "silver" + }, + before: + { + "background-color": "purple" + }, + default: + { + "background-color": "white" + }, // default value: 1px + after: + { + "background-color": "green" + }, // zone after default value + pointer: + { + "background-color": "red" + }, // circle pointer + range: + { + "background-color": "red" + } // use it if double value + }; + + $scope.monitorSize = []; // array with montage sizes per monitor + $scope.scaleDirection = []; // 1 = increase -1 = decrease + // The difference between old and original is this: + // old will have a copy of the last re-arranged monitor list + // while original will have a copy of the order returned by ZM + var oldMonitors = []; // To keep old order if user cancels after sort; + // Montage display order may be different so don't + // mangle monitors as it will affect other screens + // in Montage screen we will work with this local copy + //$scope.MontageMonitors = angular.copy ($scope.monitors); + var montageOrder = []; // This array will keep the ordering in montage view + var hiddenOrder = []; // 1 = hide, 0 = don't hide + var tempMonitors = message; + if (tempMonitors.length == 0) + { + $rootScope.zmPopup = $ionicPopup.alert( + { + title: $translate.instant('kNoMonitors'), + template: $translate.instant('kPleaseCheckCredentials'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("login"); + return; + } + + NVRDataModel.log("Inside MontageHistoryCtrl:We found " + $scope.MontageMonitors.length + " monitors"); + // $scope.MontageMonitors = NVRDataModel.applyMontageMonitorPrefs(message, 1)[0]; + var loginData = NVRDataModel.getLogin(); + // init monitors + NVRDataModel.debug(">>Initializing connkeys and images..."); + for (i = 0; i < $scope.MontageMonitors.length; i++) + { + //$scope.MontageMonitors[i].Monitor.connKey=''; + $scope.MontageMonitors[i].Monitor.eid = "-1"; + $scope.MontageMonitors[i].Monitor.connKey = (Math.floor((Math.random() * 999999) + 1)).toString(); + $scope.MontageMonitors[i].Monitor.eventUrl = 'img/noevent.png'; + $scope.MontageMonitors[i].Monitor.eid = "-1"; + $scope.MontageMonitors[i].Monitor.eventUrlTime = ""; + $scope.MontageMonitors[i].Monitor.isPaused = false; + $scope.MontageMonitors[i].Monitor.gridScale = "50"; + $scope.MontageMonitors[i].Monitor.selectStyle = ""; + $scope.MontageMonitors[i].Monitor.alarmState = 'color:rgba(0,0,0,0);'; + $scope.MontageMonitors[i].Monitor.sliderProgress = { + progress: 0 + }; + } + + // -------------------------------------------------------- + // Handling of back button in case modal is open should + // close the modal + // -------------------------------------------------------- + $ionicPlatform.registerBackButtonAction(function(e) + { + e.preventDefault(); + if ($scope.modal && $scope.modal.isShown()) + { + // switch off awake, as liveview is finished + NVRDataModel.debug("Modal is open, closing it"); + NVRDataModel.setAwake(false); + $scope.modal.remove(); + $scope.isModalActive = false; + } + else + { + NVRDataModel.debug("Modal is closed, so toggling or exiting"); + if (!$ionicSideMenuDelegate.isOpenLeft()) + { + $ionicSideMenuDelegate.toggleLeft(); + } + else + { + navigator.app.exitApp(); + } + } + }, 1000); + $scope.isRefresh = $stateParams.isRefresh; + sizeInProgress = false; + $ionicSideMenuDelegate.canDragContent(false); + $scope.LoginData = NVRDataModel.getLogin(); + $scope.monLimit = $scope.LoginData.maxMontage; + $scope.currentLimit = $scope.LoginData.maxMontage; + if ($rootScope.platformOS != 'ios') + { + NVRDataModel.log("Limiting montage to 5, thanks to Chrome's stupid connection limit"); + $scope.currentLimit = 5; + $scope.monLimit = 5; + } + $rootScope.authSession = "undefined"; + $ionicLoading.show( + { + template: $translate.instant('kNegotiatingStreamAuth'), + animation: 'fade-in', + showBackdrop: true, + duration: zm.loadingTimeout, + maxWidth: 300, + showDelay: 0 + }); + ld = NVRDataModel.getLogin(); + //console.log ("MONITORS " + JSON.stringify($scope.monitors)); + $rootScope.validMonitorId = $scope.MontageMonitors[0].Monitor.Id; + NVRDataModel.getAuthKey($rootScope.validMonitorId).then(function(success) + { + $ionicLoading.hide(); + //console.log(success); + $rootScope.authSession = success; + NVRDataModel.log("Stream authentication construction: " + $rootScope.authSession); + $timeout(function() + { + initPackery(); + readyToRun = true; + footerCollapse(); + }, zm.packeryTimer); + + }, function(error) + { + $ionicLoading.hide(); + NVRDataModel.debug("MontageHistoryCtrl: Error in authkey retrieval " + error); + //$rootScope.authSession=""; + NVRDataModel.log("MontageHistoryCtrl: Error returned Stream authentication construction. Retaining old value of: " + $rootScope.authSession); + $timeout(function() + { + initPackery(); + readyToRun = true; + footerCollapse(); + }, zm.packeryTimer); + }); + } + +}]); diff --git a/www/js/NewsCtrl.js b/www/js/NewsCtrl.js new file mode 100644 index 00000000..e1e030ec --- /dev/null +++ b/www/js/NewsCtrl.js @@ -0,0 +1,132 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console,moment*/ + +angular.module('zmApp.controllers').controller('zmApp.NewsCtrl', ['$scope', '$rootScope', '$ionicModal', 'NVRDataModel', '$ionicSideMenuDelegate', '$ionicHistory', '$state', '$http', 'zm', function($scope, $rootScope, $ionicModal, NVRDataModel, $ionicSideMenuDelegate, $ionicHistory, $state, $http, zm) +{ + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + //------------------------------------------------------------------------- + // Lets make sure we set screen dim properly as we enter + // The problem is we enter other states before we leave previous states + // from a callback perspective in ionic, so we really can't predictably + // reset power state on exit as if it is called after we enter another + // state, that effectively overwrites current view power management needs + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + // console.log("**VIEW ** News Ctrl Entered"); + NVRDataModel.setAwake(false); + + }); + + $scope.isUnread = function(itemdate) + { + var lastDate = NVRDataModel.getLatestBlogPostChecked(); + //console.log ("BLOG DATE="+itemdate+" LAST DATE="+lastDate); + //get("latestBlogPostChecked"); + if (!lastDate) return true; + var mLastDate = moment(lastDate); + var mItemDate = moment(itemdate); + //var unread = mItemDate.diff(mLastDate) >0) ? true:false; + //console.log (unread); + return (mItemDate.diff(mLastDate, 'seconds') > 0) ? true : false; + + }; + + $scope.loadPost = function(item, itemdate) + { + var lastDate = + NVRDataModel.getLatestBlogPostChecked(); //zmStorageService.get("latestBlogPostChecked"); + + if (!lastDate) + { + NVRDataModel.debug("First time checking blog posts, I see"); + NVRDataModel.setLatestBlogPostChecked(itemdate); + //zmStorageService.set("latestBlogPostChecked", itemdate); + } + else + { + NVRDataModel.debug("last post checked is " + lastDate); + NVRDataModel.debug("current post dated is " + itemdate); + + var mLastDate = moment(lastDate); + var mItemDate = moment(itemdate); + if (mItemDate.diff(mLastDate, 'seconds') > 0) + { + NVRDataModel.debug("Updating lastDate to this post"); + + NVRDataModel.setLatestBlogPostChecked(itemdate); //zmStorageService.set("latestBlogPostChecked", itemdate); + + if (itemdate == $scope.newsItems[0].date) + { + // we are reading the latest post + $rootScope.newBlogPost = ""; + } + } + + } + + window.open(item, '_blank', 'location=yes'); + return false; + }; + + $scope.newsItems = []; + + + $http.get(zm.blogUrl, + {transformResponse: function(d,h) + { + var trunc = "])}while(1);</x>"; + d = d.substr(trunc.length); + return d; + } + }) + .success(function(datastr) + { + + + // console.log ("DATA:"+data); + // + var data = JSON.parse(datastr); + for (var i = 0; i < data.payload.posts.length; i++) + { + $scope.newsItems.push( + { + title: data.payload.posts[i].title, + url: "https://medium.com/zmninja/"+data.payload.posts[i].uniqueSlug, + date: moment(data.payload.posts[i].createdAt).format("YYYY-MM-DD HH:mm:ss") + }); + } + + }); + +}]); diff --git a/www/js/PortalLoginCtrl.js b/www/js/PortalLoginCtrl.js new file mode 100644 index 00000000..6a9a9f2d --- /dev/null +++ b/www/js/PortalLoginCtrl.js @@ -0,0 +1,454 @@ +/* jshint -W041 */ +/* jshint -W083 */ +/*This is for the loop closure I am using in line 143 */ +/* jslint browser: true*/ +/* global vis,cordova,StatusBar,angular,console,moment */ +angular.module('zmApp.controllers').controller('zmApp.PortalLoginCtrl', ['$ionicPlatform', '$scope', 'zm', 'NVRDataModel', '$ionicSideMenuDelegate', '$rootScope', '$http', '$q', '$state', '$ionicLoading', '$ionicPopover', '$ionicScrollDelegate', '$ionicModal', '$timeout', 'zmAutoLogin', '$ionicHistory', '$cordovaTouchID', 'EventServer', '$translate', function($ionicPlatform, $scope, zm, NVRDataModel, $ionicSideMenuDelegate, $rootScope, $http, $q, $state, $ionicLoading, $ionicPopover, $ionicScrollDelegate, $ionicModal, $timeout, zmAutoLogin, $ionicHistory, $cordovaTouchID, EventServer, $translate) +{ + + $scope.$on('$ionicView.enter', + function() + { + + NVRDataModel.debug("Inside Portal login Enter handler"); + loginData = NVRDataModel.getLogin(); + + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + + $scope.pindata = {}; + if ($ionicSideMenuDelegate.isOpen()) + { + $ionicSideMenuDelegate.toggleLeft(); + NVRDataModel.debug("Sliding menu close"); + } + + $scope.pinPrompt = false; // if true, then PIN is displayed else skip + + if (NVRDataModel.isLoggedIn()) + { + NVRDataModel.log("User credentials are provided"); + + // You can login either via touch ID or typing in your code + if ($ionicPlatform.is('ios') && loginData.usePin) + { + $cordovaTouchID.checkSupport() + .then(function() + { + // success, TouchID supported + $cordovaTouchID.authenticate("") + .then(function() + { + NVRDataModel.log("Touch Success"); + // Don't assign pin as it may be alphanum + unlock(true); + + }, + function() + { + NVRDataModel.log("Touch Failed"); + }); + }, function(error) + { + NVRDataModel.log("TouchID not supported"); + }); + } + else // touch was not used + { + NVRDataModel.log("not checking for touchID"); + } + + if (loginData.usePin) + { + // this shows the pin prompt on screen + $scope.pinPrompt = true; + // dont call unlock, let the user type in code + + } + else // no PIN Code so go directly to auth + { + + unlock(true); + } + + } + else // login creds are not present + { + NVRDataModel.debug("PortalLogin: Not logged in, so going to login"); + if (NVRDataModel.isFirstUse()) + { + NVRDataModel.debug("First use, showing warm and fuzzy..."); + $ionicHistory.nextViewOptions( + { + disableAnimate: true, + disableBack: true + }); + $state.go('first-use'); + return; + } + else + { + if (!$rootScope.userCancelledAuth) + { + $ionicHistory.nextViewOptions( + { + disableAnimate: true, + disableBack: true + }); + $state.go("login", + { + "wizard": false + }); + return; + } + else + { + // do this only once - rest for next time + $rootScope.userCancelledAuth = false; + } + } + } + + }); + + //------------------------------------------------------------------------------- + // remove status is pin is empty + //------------------------------------------------------------------------------- + + $scope.pinChange = function() + { + if ($scope.pindata.pin == null) + { + $scope.pindata.status = ""; + } + }; + + //------------------------------------------------------------------------------- + // unlock app if PIN is correct + //------------------------------------------------------------------------------- + $scope.unlock = function() + { + // call with false meaning check for pin + unlock(false); + }; + + //------------------------------------------------------------------------ + // Aaron Lager hack - can't figure out why he gets a 401 after + // successful login and then it works after resaving + //------------------------------------------------------------------------ + function tryLoggingSecondTimeHack() + { + var d = $q.defer(); + + zmAutoLogin.doLogin("<button class='button button-clear' style='line-height: normal; min-height: 0; min-width: 0;color:#fff;' ng-click='$root.cancelAuth()'><i class='ion-close-circled'></i> " + $translate.instant('kAuthenticating') + "...</button>") + .then(function(data) // success + { + NVRDataModel.debug("2nd auth login worked"); + NVRDataModel.getAPIversion() + .then(function(data) + { + NVRDataModel.getKeyConfigParams(1); + NVRDataModel.log("2nd auth:Got API version: " + data); + $rootScope.apiVersion = data; + var ld = NVRDataModel.getLogin(); + if (NVRDataModel.versionCompare(data, zm.minAppVersion) == -1 && data != "0.0.0") + { + + $state.go('lowversion', + { + "ver": data + }); + return; + } + + if (NVRDataModel.versionCompare(data, zm.recommendedAppVersion) == -1 && data != "0.0.0") + { + + $state.go('importantmessage', + { + "ver": data + }); + return; + } + + /*if (data == "0.0.0") + { + + NVRDataModel.log("2nd Auth:API getVersion succeeded but returned 0.0.0 " + JSON.stringify(data)); + NVRDataModel.displayBanner('error', ['ZoneMinder authentication failed']); + $state.go("login", + { + "wizard": false + }); + return; + }*/ + // coming here means continue + EventServer.refresh(); + + var statetoGo = $rootScope.lastState ? $rootScope.lastState : 'montage'; + //NVRDataModel.debug ("logging state transition"); + NVRDataModel.debug("2nd Auth: Transitioning state to: " + + statetoGo + " with param " + JSON.stringify($rootScope.lastStateParam)); + $state.go(statetoGo, $rootScope.lastStateParam); + return; + + }, + function(error) + { + NVRDataModel.debug("2nd auth API failed, going to login"); + d.reject("failed 2nd auth"); + return (d.promise); + + }); + + }, + function(error) + { + NVRDataModel.debug("2nd auth hack failed, going to login"); + d.reject("failed 2nd auth"); + return (d.promise); + }); + + return (d.promise); + } + + function evaluateTappedNotification() + { + if ($rootScope.tappedNotification) + { + + var ld = NVRDataModel.getLogin(); + NVRDataModel.log("Came via push tap. onTapScreen=" + ld.onTapScreen); + //console.log ("***** NOTIFICATION TAPPED "); + $rootScope.tappedNotification = 0; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + + if (ld.onTapScreen == $translate.instant('kTapMontage')) + { + NVRDataModel.debug("Going to montage"); + $state.go("montage", + {}, + { + reload: true + }); + + return; + } + else if (ld.onTapScreen == $translate.instant('kTapEvents')) + { + NVRDataModel.debug("Going to events"); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + else // we go to live + { + NVRDataModel.debug("Going to live view "); + $state.go("monitors", + {}, + { + reload: true + }); + return; + } + } + + } + + function unlock(idVerified) + { + /* + idVerified == true means no pin check needed + == false means check PIN + */ + + NVRDataModel.debug("unlock called with check PIN=" + idVerified); + if (idVerified || ($scope.pindata.pin == loginData.pinCode)) + { + NVRDataModel.debug("PIN code entered is correct, or there is no PIN set"); + $rootScope.rand = Math.floor((Math.random() * 100000) + 1); + zmAutoLogin.stop(); //safety + zmAutoLogin.start(); + + // PIN is fine, or not set so lets login + zmAutoLogin.doLogin("<button class='button button-clear' style='line-height: normal; min-height: 0; min-width: 0;color:#fff;' ng-click='$root.cancelAuth()'><i class='ion-close-circled'></i> " + $translate.instant('kAuthenticating') + "...</button>") + .then(function(data) // success + { + NVRDataModel.debug("PortalLogin: auth success"); + + + // $state.go("login" ,{"wizard": false}); + //login was ok, so get API details + NVRDataModel.getAPIversion() + .then(function(data) + { + NVRDataModel.log("Got API version: " + data); + $rootScope.apiVersion = data; + var ld = NVRDataModel.getLogin(); + if (NVRDataModel.versionCompare(data, zm.minAppVersion) == -1 && data != "0.0.0") + { + + $state.go('lowversion', + { + "ver": data + }); + return; + } + + if (NVRDataModel.versionCompare(data, zm.recommendedAppVersion) == -1 && data != "0.0.0") + { + + // console.log (">>>>>>>>>>>>> HERE AND VERSION SAYS " +NVRDataModel.versionCompare(data, zm.recommendedAppVersion)); + //console.log ("GOING TO IMPORTANT"); + $state.go('importantmessage', + { + "ver": data + }); + return; + } + + /*if (data == "0.0.0") + { + + NVRDataModel.log("API getVersion succeeded but returned 0.0.0 " + JSON.stringify(data)); + NVRDataModel.displayBanner('error', ['ZoneMinder authentication failed']); + $state.go("login", + { + "wizard": false + }); + return; + + }*/ + // coming here means continue + // console.log (">>>>>>>>>>>>>>>>>>>>>>>>>NEVER"); + + NVRDataModel.getKeyConfigParams(1); + NVRDataModel.getTimeZone(); + EventServer.refresh(); + if ($rootScope.tappedNotification != 1) + { + console.log ("NOTIFICATION TAPPED INSIDE CHECK IS "+$rootScope.tappedNotification); + var statetoGo = $rootScope.lastState ? $rootScope.lastState : 'montage'; + NVRDataModel.debug("logging state transition"); + NVRDataModel.debug("Transitioning state to: " + + statetoGo + " with param " + JSON.stringify($rootScope.lastStateParam)); + + $state.go(statetoGo, $rootScope.lastStateParam); + return; + + } + else + evaluateTappedNotification(); + + + }, + function(error) + { // API Error + NVRDataModel.log("API Error handler: going to login getAPI returned error: " + JSON.stringify(error)); + //NVRDataModel.displayBanner('error', ['ZoneMinder authentication failed']); + + NVRDataModel.debug("Doing the Aaron Hack after 1 sec...."); + $timeout(function() + { + tryLoggingSecondTimeHack() + .then(function success(s) + { + NVRDataModel.log("2nd time login hack worked!, nothing to do"); + NVRDataModel.getTimeZone(); + }, + function error(e) + { + + if ($rootScope.apiValid == true) + { + $state.go("login", + { + "wizard": false + }); + return; + } + else + { + $state.go("invalidapi"); + return; + } + + }); + + return; + + }, 1000); + + }); + + + + }, + // coming here means auth error + // so go back to login + function(error) + { + NVRDataModel.debug("PortalLogin: error authenticating " + + JSON.stringify(error)); + if (!$rootScope.userCancelledAuth) + { + NVRDataModel.displayBanner('error', ['ZoneMinder authentication failed', 'Please check API settings']); + $ionicHistory.nextViewOptions( + { + disableAnimate: true, + disableBack: true + }); + $state.go("login", + { + "wizard": false + }); + return; + } + else + { + // if user cancelled auth I guess we go to login + $rootScope.userCancelledAuth = false; + $state.go("login", + { + "wizard": false + }); + return; + } + }); + } + else + { + $scope.pindata.status = "Invalid PIN"; + + // wobble the input box on error + var element = angular.element(document.getElementById("pin-box")); + + element.addClass("animated shake") + .one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', + function() + { + element.removeClass("animated shake"); + }); + } + } + + //------------------------------------------------------------------------------- + // Controller Main + //------------------------------------------------------------------------------- + // console.log("************* ENTERING PORTAL MAIN "); + NVRDataModel.log("Entering Portal Main"); + var loginData; + $ionicSideMenuDelegate.canDragContent(true); + +}]); diff --git a/www/js/StateCtrl.js b/www/js/StateCtrl.js new file mode 100644 index 00000000..4faec748 --- /dev/null +++ b/www/js/StateCtrl.js @@ -0,0 +1,410 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console */ + +// controller for State View + +angular.module('zmApp.controllers').controller('zmApp.StateCtrl', ['$ionicPopup', '$scope', 'zm', 'NVRDataModel', '$ionicSideMenuDelegate', '$ionicLoading', '$ionicModal', '$state', '$http', '$rootScope', '$timeout', '$ionicHistory', '$translate', function( + $ionicPopup, $scope, zm, NVRDataModel, $ionicSideMenuDelegate, $ionicLoading, $ionicModal, $state, $http, $rootScope, $timeout, $ionicHistory, $translate) +{ + + //---------------------------------------------------------------------- + // Controller main + //---------------------------------------------------------------------- + $scope.zmRun = "..."; + $scope.zmLoad = "..."; + $scope.zmDisk = "..."; + $scope.color = ""; + $scope.showDanger = false; + $scope.dangerText = [$translate.instant('kStateShowControls'), $translate.instant('kStateHideControls')]; + $scope.dangerButtonColor = ["button-positive", "button-assertive"]; + $scope.customState = ""; + $scope.allStateNames = []; + + $rootScope.zmPopup = ""; + + var loginData = NVRDataModel.getLogin(); + + var apiRun = loginData.apiurl + "/host/daemonCheck.json"; + var apiLoad = loginData.apiurl + "/host/getLoad.json"; + var apiDisk = loginData.apiurl + "/host/getDiskPercent.json"; + var apiCurrentState = loginData.apiurl + "/States.json"; + + var apiExec = loginData.apiurl + "/states/change/"; + + var inProgress = 0; // prevents user from another op if one is in progress + getRunStatus(); + + // Let's stagger this by 500ms each to see if Chrome lets these through + // This may also help if your Apache is not configured to let multiple connections through + + $timeout(function() + { + NVRDataModel.debug("invoking LoadStatus..."); + getLoadStatus(); + }, 2000); + + $timeout(function() + { + NVRDataModel.debug("invoking CurrentState..."); + getCurrentState(); + }, 4000); + + /* + $timeout(function () { + NVRDataModel.debug("invoking DiskStatus..."); + getDiskStatus(); + }, 6000); + */ + //------------------------------------------------------------------------- + // Lets make sure we set screen dim properly as we enter + // The problem is we enter other states before we leave previous states + // from a callback perspective in ionic, so we really can't predictably + // reset power state on exit as if it is called after we enter another + // state, that effectively overwrites current view power management needs + //------------------------------------------------------------------------ + $scope.$on('$ionicView.enter', function() + { + // console.log("**VIEW ** Montage Ctrl Entered"); + NVRDataModel.setAwake(false); + }); + + //--------------------------------------------------------- + // This gets the current run state custom name + // if applicable + //--------------------------------------------------------- + function getCurrentState() + { + NVRDataModel.debug("StateCtrl: getting state using " + apiCurrentState); + $http.get(apiCurrentState) + .then( + function(success) + { + NVRDataModel.debug("State results: " + JSON.stringify(success)); + var customStateArray = success.data.states; + var i = 0; + var found = false; + $scope.allStateNames = []; + for (i = 0; i < customStateArray.length; i++) + { + $scope.allStateNames.push(customStateArray[i].State.Name); + if (customStateArray[i].State.IsActive == '1') + { + $scope.customState = customStateArray[i].State.Name; + found = true; + } + } + if (!found) $scope.customState = ""; + + }, + function(error) + { + NVRDataModel.debug("StateCtrl: Error retrieving state list " + JSON.stringify(error)); + $scope.customState = ""; + + } + ); + + } + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + } + }; + + //--------------------------------------------------------- + // Allows the user to select a custom run state + //--------------------------------------------------------- + $scope.selectCustomState = function() + { + $scope.myopt = { + selectedState: "" + }; + //console.log(JSON.stringify($scope.allStateNames)); + NVRDataModel.log("List of custom states: " + JSON.stringify($scope.allStateNames)); + $rootScope.zmPopup = $ionicPopup.show( + { + scope: $scope, + template: '<ion-radio-fix ng-repeat="item in allStateNames" ng-value="item" ng-model="myopt.selectedState"> {{item}} </ion-radio-fix>', + + title: $translate.instant('kSelectRunState'), + subTitle: $translate.instant('kCurrentState') + $scope.customState ? ($translate.instant('kCurrentState') + ": " + $scope.customState) : "", + buttons: [ + { + text: $translate.instant('kButtonCancel'), + onTap: function(e) + { + return "CANCEL"; + } + + }, + { + text: $translate.instant('kButtonOk'), + onTap: function(e) + { + return "OK"; + + } + }] + }); + + // It seems invoking a popup within a popup handler + // causes issues. Doing this outside due to that reason + $rootScope.zmPopup.then(function(res) + { + // console.log("GOT : " + JSON.stringify(res)); + if (res == "OK") + { + if ($scope.myopt.selectedState != "") + controlZM($scope.myopt.selectedState); + } + }); + }; + + //---------------------------------------------------------------------- + // returns disk space in gigs taken up by events + //---------------------------------------------------------------------- + function getDiskStatus() + { + NVRDataModel.debug("StateCtrl/getDiskStatus: " + apiDisk); + $http.get(apiDisk) + .then( + function(success) + { + NVRDataModel.debug("StateCtrl/getDiskStatus: success"); + NVRDataModel.debug("Disk results: " + JSON.stringify(success)); + var obj = success.data.usage; + if (obj.Total.space != undefined) + { + $scope.zmDisk = parseFloat(obj.Total.space).toFixed(1).toString() + "G"; + } + else + { + $scope.zmDisk = "unknown"; + NVRDataModel.log("Error retrieving disk space, API returned null for obj.Total.space"); + } + + }, + function(error) + { + $scope.zmDisk = "unknown"; + // console.log("ERROR:" + JSON.stringify(error)); + NVRDataModel.log("Error retrieving DiskStatus: " + JSON.stringify(error), "error"); + } + ); + } + + //---------------------------------------------------------------------- + // returns ZM running status + //---------------------------------------------------------------------- + function getRunStatus() + { + NVRDataModel.debug("StateCtrl/getRunStatus: " + apiRun); + $http.get(apiRun) + .then( + function(success) + { + NVRDataModel.debug("StateCtrl/getRunStatus: success"); + NVRDataModel.debug("Run results: " + JSON.stringify(success)); + switch (success.data.result) + { + case 1: + $scope.zmRun = $translate.instant('kZMRunning'); + $scope.color = 'color:green;'; + break; + case 0: + $scope.zmRun = $translate.instant('kZMStopped'); + $scope.color = 'color:red;'; + break; + default: + $scope.zmRun = $translate.instant('kZMUndetermined'); + $scope.color = 'color:orange;'; + + break; + } + + // console.log("X"+success.data.result+"X"); + }, + function(error) + { + //console.log("ERROR in getRun: " + JSON.stringify(error)); + NVRDataModel.log("Error getting RunStatus " + JSON.stringify(error), "error"); + $scope.color = 'color:red;'; + $scope.zmRun = $translate.instant('kZMUndetermined'); + } + ); + + } + + //---------------------------------------------------------------------- + // gets ZM load - max[0], avg[1], min[2] + //---------------------------------------------------------------------- + function getLoadStatus() + { + NVRDataModel.debug("StateCtrl/getLoadStatus: " + apiLoad); + $http.get(apiLoad) + .then( + function(success) + { + NVRDataModel.debug("Load results: " + JSON.stringify(success)); + //console.log(JSON.stringify(success)); + // load returns 3 params - one in the middle is avg. + NVRDataModel.debug("StateCtrl/getLoadStatus: success"); + $scope.zmLoad = success.data.load[1]; + + // console.log("X"+success.data.result+"X"); + }, + function(error) + { + //console.log("ERROR in getLoad: " + JSON.stringify(error)); + NVRDataModel.log("Error retrieving loadStatus " + JSON.stringify(error), "error"); + $scope.zmLoad = 'undetermined'; + } + ); + } + + //---------------------------------------------------------------------- + // start/stop/restart ZM + //---------------------------------------------------------------------- + + function performZMoperation(str) + { + + NVRDataModel.debug("inside performZMoperation with " + str); + + $scope.zmRun = "..."; + $scope.color = 'color:orange;'; + $scope.customState = ""; + NVRDataModel.debug("StateCtrl/controlZM: POST Control command is " + apiExec + str + ".json"); + inProgress = 1; + $http.post(apiExec + str + ".json") + .then( + function(success) + { + NVRDataModel.debug("StateCtrl/controlZM: returned success"); + inProgress = 0; + switch (str) + { + case "stop": + $scope.zmRun = $translate.instant('kZMStopped'); + $scope.color = 'color:red;'; + break; + default: + $scope.zmRun = $translate.instant('kZMRunning'); + $scope.color = 'color:green;'; + getCurrentState(); + break; + + } + + }, + function(error) + { + //if (error.status) // it seems to return error with status 0 if ok + // { + //console.log("ERROR in Change State:" + JSON.stringify(error)); + NVRDataModel.debug("StateCtrl/controlZM: returned error"); + NVRDataModel.log("Error in change run state:" + JSON.stringify(error), "error"); + $scope.zmRun = $translate.instant('kZMUndetermined'); + $scope.color = 'color:orange;'; + inProgress = 0; + + }); + } + + function controlZM(str) + { + if (inProgress) + { + NVRDataModel.debug("StateCtrl/controlZM: operation in progress"); + $ionicPopup.alert( + { + title: $translate.instant('kOperationInProgressTitle'), + template: $translate.instant('kOperationInProgressBody') + '...', + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + return; + } + + var statesearch = "startstoprestart"; + + var promptstring = $translate.instant('kStateAreYouSure') + str + ' Zoneminder?'; + if (statesearch.indexOf(str) == -1) + { + promptstring = "Are you sure you want to change state to " + str; + } + + $rootScope.zmPopup = $ionicPopup.show( + { + title: $translate.instant('kPleaseConfirm'), + template: promptstring, + buttons: [ + { + text: $translate.instant('kButtonCancel'), + type: 'button-positive' + }, + { + text: $translate.instant('kButtonOk'), + type: 'button-assertive', + onTap: function(e) + { + performZMoperation(str); + } + }] + }); + + } + + // Binder so template can call controlZM + $scope.controlZM = function(str) + { + controlZM(str); + }; + + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + $scope.$on('$ionicView.leave', function() + { + console.log("**VIEW ** State Ctrl Left"); + // FIXME not the best way... + // If the user exits a view before its complete, + // make sure he can come back in and redo + inProgress = 0; + }); + + $scope.doRefresh = function() + { + console.log("***Pull to Refresh"); + NVRDataModel.debug("StateCtrl/refresh: calling getRun/Load/Disk/CurrentState"); + getRunStatus(); + $timeout(getLoadStatus, 2000); + $timeout(getCurrentState, 4000); + //$timeout (getDiskStatus,6000); + $scope.$broadcast('scroll.refreshComplete'); + + }; + +}]); diff --git a/www/js/TimelineCtrl.js b/www/js/TimelineCtrl.js new file mode 100644 index 00000000..101e7166 --- /dev/null +++ b/www/js/TimelineCtrl.js @@ -0,0 +1,1608 @@ +/* jshint -W041 */ +/* jshint -W083 */ +/*This is for the loop closure I am using in line 143 */ +/* jslint browser: true*/ +/* global vis,cordova,StatusBar,angular,console,moment */ + +// This controller creates a timeline +// It uses the visjs library, but due to performance reasons +// I've disabled pan and zoom and used buttons instead +// also limits # of items to maxItems + +// FIXME: too much redundant code between EventCtrl and Timeline +// Move to ModalCtrl and see if it works + +angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPlatform', '$scope', 'zm', 'NVRDataModel', '$ionicSideMenuDelegate', '$rootScope', '$http', '$q', 'message', '$state', '$ionicLoading', '$ionicPopover', '$ionicScrollDelegate', '$ionicModal', '$timeout', '$ionicContentBanner', '$ionicHistory', '$sce', '$stateParams', '$translate', '$ionicPopup', '$interval', function($ionicPlatform, $scope, zm, NVRDataModel, $ionicSideMenuDelegate, $rootScope, $http, $q, message, $state, $ionicLoading, $ionicPopover, $ionicScrollDelegate, $ionicModal, $timeout, $ionicContentBanner, $ionicHistory, $sce, $stateParams, $translate, $ionicPopup, $interval) +{ + + //console.log("Inside Timeline controller"); + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //---------------------------------------f------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + $scope.leftButtons = [ + { + type: 'button-icon icon ion-navicon', + tap: function(e) + { + $scope.toggleMenu(); + } + }]; + + //----------------------------------------------------------- + // Used to display date range for timeline + //----------------------------------------------------------- + $scope.prettify = function(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format('MMMM Do YYYY, ' + NVRDataModel.getTimeFormat()); + else + return moment(str).format('MMMM Do YYYY, ' + NVRDataModel.getTimeFormat()); + }; + + //----------------------------------------------------------- + // used for playback when you tap on a timeline event + //----------------------------------------------------------- + $scope.calcMsTimer = function(frames, len) + { + var myframes, mylen; + myframes = parseFloat(frames); + mylen = parseFloat(len); + // console.log ("frames " + myframes + "length " + mylen); + // console.log ("*** MS COUNT " + (1000.0/(myframes/mylen))); + return (Math.round(1000 / (myframes / mylen))); + }; + + $scope.toggleMinAlarmFrameCount = function() + { + // console.log("Toggling"); + + var ld = NVRDataModel.getLogin(); + ld.enableAlarmCount = !ld.enableAlarmCount; + + NVRDataModel.setLogin(ld); + + drawGraph(curFromDate, curToDate, curCount); + + }; + + //----------------------------------------------------------- + // Move/Zoom are used to move the timeline around + //----------------------------------------------------------- + function move(percentage) + { + var range = timeline.getWindow(); + var interval = range.end - range.start; + + timeline.setWindow( + { + start: range.start.valueOf() - interval * percentage, + end: range.end.valueOf() - interval * percentage + }); + } + + // helps to navigate to current time quickly + // after a night of heavy navigation + $scope.gotoNow = function() + { + timeline.moveTo(timeline.getCurrentTime()); + }; + + $scope.move = function(percentage) + { + move(percentage); + + }; + + //----------------------------------------- + // Move by X days + //----------------------------------------- + $scope.moveDays = function(d) + { + var range = timeline.getWindow(); + var ds = moment(range.start); + if (d > 0) + ds.add(d, 'days'); + else + ds.subtract(Math.abs(d), 'days'); + + var es = moment(ds); // clone it! + es.add(1, 'day'); + + fromDate = ds.format("YYYY-MM-DD HH:mm:ss"); + toDate = es.format("YYYY-MM-DD HH:mm:ss"); + + $scope.fromDate = fromDate; + $scope.toDate = toDate; + $rootScope.customTimelineRange = false; + NVRDataModel.log("moving by " + d + " day to " + fromDate + " upto " + toDate); + drawGraph(fromDate, toDate, maxItems); + + }; + + function eventDetails(ev) + { + $scope.event = ev; + $ionicModal.fromTemplateUrl('templates/timeline-modal.html', + { + scope: $scope, // give ModalCtrl access to this scope + animation: 'slide-in-up', + id: 'analyze', + + }) + .then(function(modal) + { + $scope.modal = modal; + + $scope.modal.show(); + + }); + } + + //-------------------------------------------------------- + // To show a modal dialog with the event tapped on in timeline + // FIXME : code repeat from Events + //-------------------------------------------------------- + function openModal(event) + { + + if ($scope.modalFromTimelineIsOpen == true) + { + // don't know why but some conflict from angular to timeline lib + // results in double modals at times + NVRDataModel.log(">>-- duplicate modal detected, preventing"); + } + + $scope.modalFromTimelineIsOpen = true; + NVRDataModel.setAwake(NVRDataModel.getKeepAwake()); + + // pass this event to ModalCtrl + $scope.currentEvent = event; + + $scope.event = event; + // in Timeline view, make sure events stick to same monitor + $scope.followSameMonitor = "1"; + + //prepareModalEvent(event.Event.Id); + + $ionicModal.fromTemplateUrl('templates/events-modal.html', + { + scope: $scope, // give ModalCtrl access to this scope + animation: 'slide-in-up', + id: 'footage' + }) + .then(function(modal) + { + $scope.modal = modal; + + $ionicLoading.show( + { + template: $translate.instant('kPleaseWait') + "...", + noBackdrop: true, + duration: 10000, + + }); + + $scope.modal.show(); + + var ld = NVRDataModel.getLogin(); + + }); + + } + + //-------------------------------------------------------- + //We need to destroy because we are instantiating + // it on open + //-------------------------------------------------------- + $scope.closeModal = function() + { + $scope.modalFromTimelineIsOpen = false; + // $interval.cancel(eventsInterval); + //$interval.cancel(segmentHandle); + NVRDataModel.debug("TimelineCtrl:Close & Destroy Modal"); + NVRDataModel.stopNetwork("TimelineCtrl: closeModal"); + NVRDataModel.setAwake(false); + if ($scope.modal !== undefined) + { + $scope.modal.remove(); + } + + }; + + /* $scope.toggleGapless = function() + { + console.log ("GAPLESS TOGGLE"); + $scope.loginData.gapless = !$scope.loginData.gapless; + NVRDataModel.setLogin($scope.loginData); + + };*/ + + //------------------------------------------------------------------------- + // called when user switches to background + //------------------------------------------------------------------------- + function onPause() + { + NVRDataModel.debug("TimelineCtrl:onpause called"); + $interval.cancel(updateInterval); + // console.log("*** Moving to Background ***"); // Handle the pause event + + if ($scope.popover) $scope.popover.remove(); + + } + + //-------------------------------------------------------- + // This function is called by the graph ontapped function + // which in turn calls openModal + //-------------------------------------------------------- + + function showEvent(event) + { + + // in context of angular + + $timeout(function() + { + openModal(event); + }); + + } + + $rootScope.$on('tz-updated', function() + { + $scope.tzAbbr = NVRDataModel.getTimeZoneNow(); + NVRDataModel.debug("Timezone API updated timezone to " + NVRDataModel.getTimeZoneNow()); + }); + + //------------------------------------------------- + // Make sure we delete the timeline + // This may be redundant as the root view gets + // destroyed but no harm + //------------------------------------------------- + $scope.$on('$ionicView.leave', function() + { + + if (timeline) + { + $interval.cancel(updateInterval); + timeline.destroy(); + console.log("**Destroying Timeline"); + + } + }); + + /*$scope.$on('$ionicView.enter', function() { + + + + + });*/ + + $scope.$on('$ionicView.beforeEnter', function() + { + + //$ionicHistory.clearCache(); + //$ionicHistory.clearHistory(); + timeline = ''; + $scope.newEvents = ''; + + }); + + //------------------------------------------------- + // FIXME: shitty hackery -- Im using a rootScope + // to know if you just went to custom range + // and back. Fix this, really. + // So anyway, if you did select a custom range + // then we "go back" to timeline, which is when + // we come here - so make sure we update the + // graph range + //------------------------------------------------- + + $scope.$on('$ionicView.afterEnter', function() + { + + $scope.monitors = message; + //console.log("***AFTER ENTER"); + + $scope.follow = { + 'time': NVRDataModel.getLogin().followTimeLine + }; + + $interval.cancel(updateInterval); + + // Make sure sliding for menu is disabled so it + // does not interfere with graph panning + $ionicSideMenuDelegate.canDragContent(false); + var ld = NVRDataModel.getLogin(); + maxItemsConf = ($rootScope.platformOS == 'desktop') ? zm.graphDesktopItemMax : zm.graphItemMax; + maxItems = ld.graphSize || maxItemsConf; + NVRDataModel.log("Graph items to draw is " + maxItems); + $scope.maxItems = maxItems; + $scope.translationData = { + maxItemsVal: maxItems + }; + + $scope.graphLoaded = false; + NVRDataModel.debug("TimelineCtrl/drawGraph: graphLoaded is " + $scope.graphLoaded); + + //latestDateDrawn = moment().locale('en').format("YYYY-MM-DD HH:mm:ss"); + $scope.modalFromTimelineIsOpen = false; + //var tempMon = message; + + // lets timeline.onget the abbreviated version of TZ to display + if (NVRDataModel.getLogin().useLocalTimeZone) + { + $scope.tzAbbr = moment().tz(moment.tz.guess()).zoneAbbr(); + } + else + { + $scope.tzAbbr = moment().tz(NVRDataModel.getTimeZoneNow()).zoneAbbr(); + } + + //console.log ("TIMELINE MONITORS: " + JSON.stringify(message)); + //var ld = NVRDataModel.getLogin(); + $scope.loginData = NVRDataModel.getLogin(); + + /* if (ld.persistMontageOrder) { + var iMon = NVRDataModel.applyMontageMonitorPrefs(tempMon, 2); + $scope.monitors = iMon[0]; + } else*/ + + //console.log ("MONITORS:"+JSON.stringify($scope.monitors)); + + if ($rootScope.customTimelineRange) + { + $scope.currentMode = 'custom'; + //console.log("***** CUSTOM RANGE"); + if (moment($rootScope.fromString).isValid() && + moment($rootScope.toString).isValid()) + { + // console.log("FROM & TO IS CUSTOM"); + fromDate = $rootScope.fromString; + toDate = $rootScope.toString; + $scope.fromDate = fromDate; + $scope.toDate = toDate; + drawGraph(fromDate, toDate, maxItems); + } + else + { + console.log("From:" + $rootScope.fromString + " To:" + $rootScope.toString); + //console.log("FROM & TO IS CUSTOM INVALID"); + + if (NVRDataModel.getLogin().useLocalTimeZone) + { + fromDate = moment().startOf('day').format("YYYY-MM-DD HH:mm:ss"); + toDate = moment().endOf('day').format("YYYY-MM-DD HH:mm:ss"); + } + else + { + fromDate = moment().tz(NVRDataModel.getTimeZoneNow()).startOf('day').format("YYYY-MM-DD HH:mm:ss"); + toDate = moment().tz(NVRDataModel.getTimeZoneNow()).endOf('day').format("YYYY-MM-DD HH:mm:ss"); + } + + drawGraph(fromDate, toDate, maxItems); + } + } + else + { + $scope.currentMode = 'day'; + + if (NVRDataModel.getLogin().useLocalTimeZone) + { + fromDate = moment().startOf('day').format("YYYY-MM-DD HH:mm:ss"); + toDate = moment().endOf('day').format("YYYY-MM-DD HH:mm:ss"); + } + else + { + fromDate = moment().tz(NVRDataModel.getTimeZoneNow()).startOf('day').format("YYYY-MM-DD HH:mm:ss"); + toDate = moment().tz(NVRDataModel.getTimeZoneNow()).endOf('day').format("YYYY-MM-DD HH:mm:ss"); + } + drawGraph(fromDate, toDate, maxItems); + } + + $ionicPopover.fromTemplateUrl('templates/timeline-popover.html', + { + scope: $scope, + }).then(function(popover) + { + $scope.popover = popover; + }); + + // -------------------------------------------------------- + // Handling of back button in case modal is open should + // close the modal + // -------------------------------------------------------- + + $ionicPlatform.registerBackButtonAction(function(e) + { + e.preventDefault(); + if ($scope.modal != undefined && $scope.modal.isShown()) + { + // switch off awake, as liveview is finished + NVRDataModel.debug("Modal is open, closing it"); + NVRDataModel.setAwake(false); + $scope.modal.remove(); + } + else + { + NVRDataModel.debug("Modal is closed, so toggling or exiting"); + if (!$ionicSideMenuDelegate.isOpenLeft()) + { + $ionicSideMenuDelegate.toggleLeft(); + + } + else + { + navigator.app.exitApp(); + } + + } + + }, 1000); + + }); + + //------------------------------------------------- + // Controller main + //------------------------------------------------- + + var graphIndex; + var updateInterval; + var lastTimeForEvent; + var groups, graphData; + var isProcessNewEventsWaiting = false; + var options; + + $scope.mycarousel = { + index: 0 + }; + $scope.ionRange = { + index: 1 + }; + + var curFromDate, curToDate, curCount; + + document.addEventListener("pause", onPause, false); + + // FIXME: Timeline awake to avoid graph redrawing + NVRDataModel.setAwake(NVRDataModel.getKeepAwake()); + + // fromDate and toDate will be used to plot the range for the graph + // We start in day mode + // + var fromDate, toDate; + + fromDate = moment().tz(NVRDataModel.getLogin().useLocalTimeZone ? NVRDataModel.getLocalTimeZoneNow() : NVRDataModel.getTimeZoneNow()).startOf('day').format("YYYY-MM-DD HH:mm:ss"); + toDate = moment().tz(NVRDataModel.getLogin().useLocalTimeZone ? NVRDataModel.getLocalTimeZoneNow() : NVRDataModel.getTimeZoneNow()).endOf('day').format("YYYY-MM-DD HH:mm:ss"); + + $scope.fromDate = fromDate; + $scope.toDate = toDate; + + // maxItems will be ignored during timeline draw if its desktop + var maxItemsConf; + + var ld = NVRDataModel.getLogin(); + var maxItems; + + //flat colors for graph - https://flatuicolors.com http://www.flatuicolorpicker.com + var colors = ['#3498db', '#E57373', '#EB974E', '#95A5A6', '#e74c3c', '#03C9A9', ]; + + var container; + container = angular.element(document.getElementById('visualization')); + var timeline; + + //console.log ("RETURNING MONITORS " + JSON.stringify($scope.monitors)); + //$scope.monitors = message; + + //console.log ("MONITOR DATA AFTER APPLYING : " + JSON.stringify($scope.monitors)); + + $scope.navControls = false; + var navControls = false; + + //drawGraph(fromDate, toDate, maxItems); + //dummyDrawGraph(fromDate, toDate,maxItems); + + //------------------------------------------------- + // Rest graph to sane state after you went + // wild zooming and panning :-) + //------------------------------------------------- + $scope.fit = function() + { + timeline.fit(); + }; + + $scope.toggleNav = function() + { + if (navControls == true) + { + navControls = !navControls; + // $scope.navControls = navControls; + // give out animation time + $timeout(function() + { + $scope.navControls = navControls; + }, 2000); + } + else + { + navControls = !navControls; + $scope.navControls = navControls; + } + var element = angular.element(document.getElementById("timeline-ctrl")); + + if (navControls) + { + element.removeClass("animated bounceOutLeft"); + element.addClass("animated bounceInRight"); + } + else + { + element.removeClass("animated bounceInRight"); + element.addClass("animated bounceOutLeft"); + } + + }; + + function shortenTime(str) + { + if (NVRDataModel.getLogin().useLocalTimeZone) + return moment.tz(str, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()).format(NVRDataModel.getTimeFormat()); + else + return moment(str).format(NVRDataModel.getTimeFormat()); + } + + $scope.toggleFollowTime = function() + { + /*if ($scope.currentMode != 'day') { + $rootScope.zmPopup = $ionicPopup.alert({ + title: $translate.instant('kError'), + template: $translate.instant('kFollowError') + }); + return; + }*/ + $scope.follow.time = !$scope.follow.time; + var loginData = NVRDataModel.getLogin(); + loginData.followTimeLine = $scope.follow.time; + NVRDataModel.setLogin(loginData); + }; + //------------------------------------------------- + // Called with day/week/month + // so we can redraw the graph + //------------------------------------------------- + + $scope.buttonClicked = function(index) + { + //console.log (index); + if (index == 0) //month + { + $scope.follow.time = NVRDataModel.getLogin().followTimeLine; + $scope.currentMode = "month"; + NVRDataModel.log("Month view"); + $rootScope.customTimelineRange = false; + + toDate = moment().format("YYYY-MM-DD HH:mm:ss"); + fromDate = moment().subtract(1, 'month').startOf('day').format("YYYY-MM-DD HH:mm:ss"); + $scope.fromDate = fromDate; + $scope.toDate = toDate; + drawGraph(fromDate, toDate, maxItems); + } + else if (index == 1) //week + { + $scope.follow.time = NVRDataModel.getLogin().followTimeLine; + $scope.currentMode = "week"; + $rootScope.customTimelineRange = false; + NVRDataModel.log("Week view"); + toDate = moment().tz(NVRDataModel.getLogin().useLocalTimeZone ? NVRDataModel.getLocalTimeZoneNow() : NVRDataModel.getTimeZoneNow()).format("YYYY-MM-DD HH:mm:ss"); + fromDate = moment().tz(NVRDataModel.getLogin().useLocalTimeZone ? NVRDataModel.getLocalTimeZoneNow() : NVRDataModel.getTimeZoneNow()).subtract(1, 'week').startOf('day').format("YYYY-MM-DD HH:mm:ss"); + $scope.fromDate = fromDate; + $scope.toDate = toDate; + drawGraph(fromDate, toDate, maxItems); + } + else if (index == 2) //day + { + + $scope.currentMode = "day"; + $rootScope.customTimelineRange = false; + NVRDataModel.log("Day view"); + //toDate = moment().format("YYYY-MM-DD HH:mm:ss"); + fromDate = moment().tz(NVRDataModel.getLogin().useLocalTimeZone ? NVRDataModel.getLocalTimeZoneNow() : NVRDataModel.getTimeZoneNow()).startOf('day').format("YYYY-MM-DD HH:mm:ss"); + toDate = moment().tz(NVRDataModel.getLogin().useLocalTimeZone ? NVRDataModel.getLocalTimeZoneNow() : NVRDataModel.getTimeZoneNow()).endOf('day').format("YYYY-MM-DD HH:mm:ss"); + $scope.fromDate = fromDate; + $scope.toDate = toDate; + drawGraph(fromDate, toDate, maxItems); + } + else // custom + { + $scope.follow.time = NVRDataModel.getLogin().followTimeLine; + $scope.currentMode = "custom"; + $rootScope.customTimelineRange = true; + $state.go('events-date-time-filter'); + return; + } + + }; + + /** + * [processNewEvents is called every X seconds when dynamic update is on. X = 10 for now] + * @return {[type]} + */ + function processNewEvents() + { + + //safeguard in the event http calls are still going on + if (!$scope.follow.time || isProcessNewEventsWaiting) return; + + var ld = NVRDataModel.getLogin(); + + // check for last 2 minutes to account for late DB updates and what not. 5 mins was likely enough + // + + // make sure these are server time + var from = moment(lastTimeForEvent).tz(NVRDataModel.getTimeZoneNow()); + from = from.subtract(2, 'minutes').locale('en').format("YYYY-MM-DD HH:mm:ss"); + + var to = moment(lastTimeForEvent).tz(NVRDataModel.getTimeZoneNow()); + to = to.locale('en').format("YYYY-MM-DD HH:mm:ss"); + + lastTimeForEvent = moment().tz(NVRDataModel.getLogin().useLocalTimeZone ? NVRDataModel.getLocalTimeZoneNow() : NVRDataModel.getTimeZoneNow()); + + // FIXME: totally ignoring event pages - hoping it wont be more than 100 or 150 whatever + // the events per page limit is. Why? laziness. + // + var completedEvents = ld.apiurl + '/events/index/EndTime >=:' + from; + // we can add alarmCount as this is really for completed events + //completedEvents = completedEvents + "/AlarmFrames >=:" + (ld.enableAlarmCount ? ld.minAlarmCount : 0); + + completedEvents = completedEvents + ".json"; + + // now get currently ongoing events + // as it turns out various events get stored withn null and never recover + // so, lets limiy to 15 m + // + + var st = moment(lastTimeForEvent).tz(NVRDataModel.getTimeZoneNow()); + st = st.subtract(10, 'minutes').locale('en').format("YYYY-MM-DD HH:mm:ss"); + var ongoingEvents = ld.apiurl + '/events/index/StartTime >=:' + st + '/EndTime =:.json'; + //NVRDataModel.debug("Getting incremental events using: " + completedEvents); + + NVRDataModel.debug("Completed events API:" + completedEvents); + NVRDataModel.debug("Ongoing events API:+" + ongoingEvents); + + isProcessNewEventsWaiting = true; + + var $httpApi = $http.get(completedEvents); + var $httpOngoing = $http.get(ongoingEvents); + + $q.all([$httpApi, $httpOngoing]) + .then(function(dataarray) + { + + var myevents = dataarray[0].data.events; + + if (dataarray.length > 1) + { + myevents = myevents.concat(dataarray[1].data.events); + + } + + $scope.newEvents = ''; + var localNewEvents = ''; + //console.log ("GOT "+JSON.stringify(data)); + + for (var j = 0; j < myevents.length; j++) + { + + // these are all in server timezone but no TZ + + myevents[j].Event.StartTime = moment.tz(myevents[j].Event.StartTime, NVRDataModel.getTimeZoneNow()).format('YYYY-MM-DD HH:mm:ss'); + + myevents[j].Event.EndTime = moment.tz(myevents[j].Event.EndTime, NVRDataModel.getTimeZoneNow()).format('YYYY-MM-DD HH:mm:ss'); + + var itm = graphData.get(myevents[j].Event.Id); + if (itm) + { + // console.log(myevents[j].Event.Id + " already exists, updating params"); + + var content = "<span class='my-vis-font'>" + "(" + myevents[j].Event.Id + ")" + myevents[j].Event.Notes + " " + $translate.instant('kRecordingProgress') + "</span>"; + + var style; + var recordingInProgress = false; + + if (moment(myevents[j].Event.EndTime).isValid()) // recording over + { + //console.log ("EVENT "+myevents[j].Event.Id+" emded at "+myevents[j].Event.EndTime); + + content = "<span class='my-vis-font'>" + "( <i class='ion-android-notifications'></i>" + myevents[j].Event.AlarmFrames + ") " + " (" + myevents[j].Event.Id + ") " + myevents[j].Event.Notes + "</span>"; + + style = "background-color:" + colors[parseInt(myevents[j].Event.MonitorId) % colors.length] + + ";border-color:" + colors[parseInt(myevents[j].Event.MonitorId) % colors.length]; + } + else // still recording + { + + var tze; + tze = moment().tz(NVRDataModel.getTimeZoneNow()); + + myevents[j].Event.EndTime = tze.format('YYYY-MM-DD HH:mm:ss'); + + //console.log ("END TIME = "+ myevents[j].Event.EndTime); + + style = "background-color:orange"; + recordingInProgress = true; + + } + + // right at this point we need to decide if we keep or remove this event + // + + if (ld.enableAlarmCount && ld.minAlarmCount > myevents[j].Event.AlarmFrames && !recordingInProgress) + { + // remove + NVRDataModel.debug("Removing Event:" + myevents[j].Event.Id + "as it doesn't have " + myevents[j].Event.AlarmFrames + " alarm frames"); + // var old = timeline.getWindow(); + graphData.remove(myevents[j].Event.Id); + // timeline.setWindow (old.start, old.end); + } + else + { + + var tzs1, tze1; + if (NVRDataModel.getLogin().useLocalTimeZone) + { + tzs1 = moment.tz(myevents[j].Event.StartTime, NVRDataModel.getTimeZoneNow()).tz(NVRDataModel.getLocalTimeZoneNow()); + tze1 = moment.tz(myevents[j].Event.EndTime, NVRDataModel.getTimeZoneNow()).tz(NVRDataModel.getLocalTimeZoneNow()); + } + else + { + tzs1 = moment.tz(myevents[j].Event.StartTime, NVRDataModel.getTimeZoneNow()); + tze1 = moment.tz(myevents[j].Event.EndTime, NVRDataModel.getTimeZoneNow()); + } + + //tzs1 = tzs1.format("YYYY-MM-DD HH:mm:ss"); + //tze1 = tze1.format("YYYY-MM-DD HH:mm:ss"); + + NVRDataModel.debug("Updating Event:" + myevents[j].Event.Id + "StartTime:" + tzs1.format() + " EndTime:" + tze1.format()); + graphData.update( + { + id: myevents[j].Event.Id, + content: content, + start: tzs1, + // start: myevents[j].Event.StartTime, + // end: myevents[j].Event.EndTime, + end: tze1, + //group: myevents[j].Event.MonitorId, + //type: "range", + style: style, + myframes: myevents[j].Event.Frames, + mydur: myevents[j].Event.Length, + myeid: myevents[j].Event.Id, + myename: myevents[j].Event.Name, + myvideo: myevents[j].Event.DefaultVideo, + myevent: myevents[j] + + }); + + //timeline.focus(myevents[j].Event.Id); + // + timeline.moveTo(timeline.getCurrentTime()); + //console.log ("Focus EID="+myevents[j].Event.Id); + localNewEvents = localNewEvents + NVRDataModel.getMonitorName(myevents[j].Event.MonitorId) + '@' + shortenTime(myevents[j].Event.StartTime) + ' (' + myevents[j].Event.Id + '),'; + + } + + } + else + { // event is new + + var isBeingRecorded = false; + var idfound = false; + for (var ii = 0; ii < $scope.monitors.length; ii++) + { + if ($scope.monitors[ii].Monitor.Id == myevents[j].Event.MonitorId && NVRDataModel.isNotHidden(myevents[j].Event.MonitorId)) + { + idfound = true; + break; + } + } + + if (idfound) + { + + myevents[j].Event.MonitorName = NVRDataModel.getMonitorName(myevents[j].Event.MonitorId); + + myevents[j].Event.streamingURL = NVRDataModel.getStreamingURL(myevents[j].Event.MonitorId); + myevents[j].Event.baseURL = NVRDataModel.getBaseURL(myevents[j].Event.MonitorId); + myevents[j].Event.imageMode = NVRDataModel.getImageMode(myevents[j].Event.MonitorId); + if (NVRDataModel.getLogin().url != myevents[j].Event.baseURL) + { + + myevents[j].Event.baseURL = NVRDataModel.getLogin().url; + } + + if (typeof myevents[j].Event.DefaultVideo === 'undefined') + // console.log (JSON.stringify(myevents[j])); + myevents[j].Event.DefaultVideo = ""; + + // now lets make sure we don't infinitely increase + + if (graphIndex >= curCount) + //if (1) + { + var mv = graphData.min('id'); + //console.log("MIN="+JSON.stringify(mv)); + if (mv) + { + graphData.remove(mv.id); + graphIndex--; + NVRDataModel.debug("Removed Event " + mv.id + " to make space"); + } + + } + + // since this is a new add its possible dates are not defined + if (!moment(myevents[j].Event.StartTime).isValid()) + { + NVRDataModel.log("Event:" + myevents[j].Event.Id + "-Invalid Start time - this should really not happen "); + + } + + if (!moment(myevents[j].Event.EndTime).isValid()) + { + var t1 = moment().tz(NVRDataModel.getTimeZoneNow()); + + myevents[j].Event.EndTime = t1.format('YYYY-MM-DD HH:mm:ss'); + + NVRDataModel.debug("Event:" + myevents[j].Event.Id + "-End time is invalid, setting to current time"); + + isBeingRecorded = true; + + } + + // if range doesn't allow for current time, we need to fix that + /*if (moment(options.max).isBefore(moment())) { + // console.log("Adjusting Range to fit in new event"); + options.max = moment().add('1', 'hours').locale('en').format("YYYY-MM-DD HH:mm:ss"); + timeline.setOptions(options); + }*/ + + var eventText = "<span class='my-vis-font'>" + "( <i class='ion-android-notifications'></i>" + (myevents[j].Event.AlarmFrames || ' unknown ') + ") " + myevents[j].Event.Notes + "</span>"; + + if (isBeingRecorded) + { + eventText = "<span class='my-vis-font'>" + "(" + myevents[j].Event.Id + ") " + myevents[j].Event.Notes + " " + $translate.instant('kRecordingProgress') + "</span>"; + } + + // since we concated, its possible events may be repeated + if (!graphData.get(myevents[j].Event.Id)) + { + + localNewEvents = localNewEvents + NVRDataModel.getMonitorName(myevents[j].Event.MonitorId) + '@' + shortenTime(myevents[j].Event.StartTime) + ' (' + myevents[j].Event.Id + '),'; + + var tzs2, tze2; + if (NVRDataModel.getLogin().useLocalTimeZone) + { + tzs2 = moment.tz(myevents[j].Event.StartTime, NVRDataModel.getTimeZoneNow()).tz(NVRDataModel.getTimeZoneNow()).tz(NVRDataModel.getLocalTimeZoneNow()); + tze2 = moment.tz(myevents[j].Event.EndTime, NVRDataModel.getTimeZoneNow()).tz(NVRDataModel.getLocalTimeZoneNow()); + } + else + { + tzs2 = moment.tz(myevents[j].Event.StartTime, NVRDataModel.getTimeZoneNow()); + tze2 = moment.tz(myevents[j].Event.EndTime, NVRDataModel.getTimeZoneNow()); + } + + //tzs2 = tzs2.format("YYYY-MM-DD HH:mm:ss"); + //tze2 = tze2.format("YYYY-MM-DD HH:mm:ss"); + + NVRDataModel.debug(">>> " + myevents[j].Event.Id + " New event updating graph " + " from:" + tzs2.format() + " to:" + tze2.format()); + + graphData.add( + { + + id: myevents[j].Event.Id, + content: eventText, + start: tzs2, + //start: myevents[j].Event.StartTime, + // end: myevents[j].Event.EndTime, + end: tze2, + group: myevents[j].Event.MonitorId, + style: "background-color:orange", + //type: "range", + + myframes: myevents[j].Event.Frames, + mydur: myevents[j].Event.Length, + myeid: myevents[j].Event.Id, + myename: myevents[j].Event.Name, + myvideo: myevents[j].Event.DefaultVideo, + myevent: myevents[j] + + }); + graphIndex++; + //timeline.focus(myevents[j].Event.Id); + timeline.moveTo(timeline.getCurrentTime()); + } + + //options.max = moment(fromDate).locale('en').format("YYYY-MM-DD HH:mm:ss"); + + } //idfound + + } // new event + + } // for j + + // At this stage, see if we need to display new events + if (localNewEvents.length > 0) + { + localNewEvents = $translate.instant('kLatestEvents') + ':' + localNewEvents; + localNewEvents = localNewEvents.slice(0, -1); + $scope.newEvents = localNewEvents; + } + isProcessNewEventsWaiting = false; + + }, + function(err) + { + NVRDataModel.debug("Error getting incremental timeline data"); + isProcessNewEventsWaiting = false; + + }); + + // check all events that started 10+10 seconds ago + + } + + //------------------------------------------------- + // This function draws the graph + //------------------------------------------------- + + function drawGraph(fromDate, toDate, count) + { + + console.log("INSIDE DRAW"); + + $scope.newEvents = ""; + // we only need this for day mode + $interval.cancel(updateInterval); + + curFromDate = fromDate; + curToDate = toDate; + curCount = count; + + var isFirstItem = true; + + var fromDateNoLang = moment(fromDate).locale('en').format("YYYY-MM-DD HH:mm:ss"); + var toDateNoLang = moment(toDate).locale('en').format("YYYY-MM-DD HH:mm:ss"); + + //latestDateDrawn =toDateNoLang; + + $ionicLoading.show( + { + template: $translate.instant('kLoadingGraph') + "...", + animation: 'fade-in', + showBackdrop: true, + maxWidth: 200, + showDelay: 0, + duration: zm.loadingTimeout, //specifically for Android - http seems to get stuck at times + }); + + NVRDataModel.log("TimelineCtrl/drawgraph: from->" + fromDateNoLang + " to->" + toDateNoLang + " count:" + count); + $scope.graphLoaded = false; + NVRDataModel.debug("TimelineCtrl/drawgraph: graphLoaded:" + $scope.graphLoaded); + + if (timeline) + { + NVRDataModel.debug("TimelineCtrl/drawgraph: destroying timeline as it exists"); + timeline.destroy(); + } + + groups = new vis.DataSet(); + graphData = new vis.DataSet(); + //console.log ("AFTER VIS"); + + var tzs, tze; + + // lets scope the time graph to either local or remote time zone + + if (NVRDataModel.getLogin().useLocalTimeZone) + { + tzs = moment.tz(fromDate, NVRDataModel.getTimeZoneNow()).tz(NVRDataModel.getLocalTimeZoneNow()); + tze = moment.tz(toDate, NVRDataModel.getTimeZoneNow()).tz(NVRDataModel.getLocalTimeZoneNow()); + } + else + { + tzs = moment.tz(fromDate, NVRDataModel.getTimeZoneNow()); + tze = moment.tz(toDate, NVRDataModel.getTimeZoneNow()); + } + + //tzs = tzs.format("YYYY-MM-DD HH:mm:ss"); + //tze = tze.format("YYYY-MM-DD HH:mm:ss"); + + options = { + + showCurrentTime: true, + editable: false, + moment: function(date) + { + + //var t; + if (NVRDataModel.getLogin().useLocalTimeZone) + //if (0) + return moment.tz(date, NVRDataModel.getTimeZoneNow()).tz(NVRDataModel.getLocalTimeZoneNow()); + else + // typecast to server time zone - its in server time anyway + return moment.tz(date, NVRDataModel.getTimeZoneNow()); + }, + //throttleRedraw: 100, + moveable: true, + zoomable: true, + selectable: true, + start: tzs, + end: tze, + orientation: 'top', + min: tzs, + //max: tze, + zoomMin: 5 * 60 * 1000, // 1 min + stack: false, + format: + { + minorLabels: + { + minute: NVRDataModel.getTimeFormat(), + hour: NVRDataModel.getTimeFormat(), + second: 's', + }, + majorLabels: + { + second: "D MMM " + NVRDataModel.getTimeFormat(), + } + }, + + }; + + graphIndex = 1; // will be used for graph ID + + //console.log ("**NOLANG" + fromDateNoLang + " " + toDateNoLang); + + NVRDataModel.getEventsPages(0, fromDateNoLang, toDateNoLang) + .then(function(data) + { + var pages = data.pageCount || 1; + var itemsPerPage = parseInt(data.limit); + var iterCount; + + // So iterCount is the # of HTTP calls I need to make + iterCount = Math.max(Math.round(count / itemsPerPage), 1); + NVRDataModel.debug("TimelineCtrl/drawGraph: pages of data: " + pages + " items per page: " + itemsPerPage); + NVRDataModel.debug("TimelineCtrl/drawGraph: I will make " + iterCount + " HTTP Requests to get all graph data"); + + // I've restructured this part. I was initially using vis DataSets + // for dynamic binding which was easier, but due to performance reasons + // I am waiting for the full data to load before I draw + var promises = []; + while ((pages > 0) && (iterCount > 0)) + { + var promise = NVRDataModel.getEvents(0, pages, "none", fromDateNoLang, toDateNoLang); + promises.push(promise); + pages--; + iterCount--; + + } + + $q.all(promises) + .then(function(data) + { + NVRDataModel.debug("TimelineCtrl/drawgraph: all pages of graph data received"); + graphIndex = 0; + NVRDataModel.log("Creating " + $scope.monitors.length + " groups for the graph"); + // create groups + for (var g = 0; g < $scope.monitors.length; g++) + { + groups.add( + { + id: $scope.monitors[g].Monitor.Id, + //mid: $scope.monitors[g].Monitor.Id, + content: NVRDataModel.getMonitorName($scope.monitors[g].Monitor.Id), + order: $scope.monitors[g].Monitor.Sequence + }); + NVRDataModel.debug("TimelineCtrl/drawgraph:Adding group " + + NVRDataModel.getMonitorName($scope.monitors[g].Monitor.Id)); + } + + for (var j = 0; j < data.length; j++) + { + var myevents = data[j]; + + if (graphIndex > count) + { + NVRDataModel.log("Exiting page count graph - reached limit of " + count); + break; + + } + + for (var i = 0; i < myevents.length; i++) + { + + // make sure group id exists before adding + var idfound = true; + var ld = NVRDataModel.getLogin(); + + if (ld.persistMontageOrder) + { + + idfound = false; + for (var ii = 0; ii < $scope.monitors.length; ii++) + { + if ($scope.monitors[ii].Monitor.Id == myevents[i].Event.MonitorId && NVRDataModel.isNotHidden(myevents[i].Event.MonitorId)) + { + idfound = true; + //console.log ("****************** ID MATCH " + graphIndex); + + break; + } + } + } + + myevents[i].Event.MonitorName = NVRDataModel.getMonitorName(myevents[i].Event.MonitorId); + // now construct base path + + myevents[i].Event.streamingURL = NVRDataModel.getStreamingURL(myevents[i].Event.MonitorId); + myevents[i].Event.baseURL = NVRDataModel.getBaseURL(myevents[i].Event.MonitorId); + myevents[i].Event.imageMode = NVRDataModel.getImageMode(myevents[i].Event.MonitorId); + if (NVRDataModel.getLogin().url != myevents[i].Event.baseURL) + { + //NVRDataModel.debug ("Multi server, changing base"); + myevents[i].Event.baseURL = NVRDataModel.getLogin().url; + + } + // console.log ("***** MULTISERVER STREAMING URL FOR EVENTS " + myevents[i].Event.streamingURL); + + // console.log ("***** MULTISERVER BASE URL FOR EVENTS " + myevents[i].Event.baseURL); + + if (idfound) + { + + if (typeof myevents[i].Event.DefaultVideo === 'undefined') + // console.log (JSON.stringify(myevents[i])); + myevents[i].Event.DefaultVideo = ""; + + //console.log ("ADDING "+myevents[i].Event.StartTime+"->"+myevents[i].Event.EndTime); + + var tzs, tze; + if (NVRDataModel.getLogin().useLocalTimeZone) + { + tzs = moment.tz(myevents[i].Event.StartTime, NVRDataModel.getTimeZoneNow()).tz(NVRDataModel.getLocalTimeZoneNow()); + tze = moment.tz(myevents[i].Event.EndTime, NVRDataModel.getTimeZoneNow()).tz(NVRDataModel.getLocalTimeZoneNow()); + } + else + { + tzs = moment.tz(myevents[i].Event.StartTime, NVRDataModel.getTimeZoneNow()); + tze = moment.tz(myevents[i].Event.EndTime, NVRDataModel.getTimeZoneNow()); + } + + //console.log ("ADDED "+tzs+" " +tze); + graphData.add( + { + //id: graphIndex, + id: myevents[i].Event.Id, + content: "<span class='my-vis-font'>" + "( <i class='ion-android-notifications'></i>" + myevents[i].Event.AlarmFrames + ") " + "(" + myevents[j].Event.Id + ") " + myevents[i].Event.Notes + "</span>", + + start: tzs, + //start: myevents[i].Event.StartTime, + //end: myevents[i].Event.EndTime, + end: tze, + group: myevents[i].Event.MonitorId, + //type: "range", + style: "background-color:" + colors[parseInt(myevents[i].Event.MonitorId) % colors.length] + + ";border-color:" + colors[parseInt(myevents[i].Event.MonitorId) % colors.length], + myframes: myevents[i].Event.Frames, + mydur: myevents[i].Event.Length, + myeid: myevents[i].Event.Id, + myename: myevents[i].Event.Name, + myvideo: myevents[i].Event.DefaultVideo, + myevent: myevents[i] + + }); + graphIndex++; + } + else + { + //console.log ("SKIPPED GRAPH ID " + graphIndex); + } + + if (graphIndex > count) + { + NVRDataModel.log("Exiting event graph - reached limit of " + count); + break; + + } + + } + } + + console.log(">>>>> CREATING NEW TIMELINE with " + JSON.stringify(options)); + timeline = new vis.Timeline(container[0], null, options); + // console.log ("GRAPH DATA"); + timeline.setItems(graphData); + // console.log ("GROUPS"); + timeline.setGroups(groups); + + if (NVRDataModel.getLogin().timelineScale == -1) + + { + // console.log ("SCALE NOT FOUND"); + + timeline.fit(); + } + else + { + timeline.fit(); + + /*var d = NVRDataModel.getLogin().timelineScale; + console.log ("SCALE FOUND "+d+" SECONDS"); + var w = timeline.getWindow(); + console.log ("Original s="+w.start+" e="+w.end); + + + var s = moment.tz(w.end, NVRDataModel.getTimeZoneNow()).subtract(d,'seconds').tz(moment.tz.guess()); + + //var s = moment(w.start).format("YYYY-MM-DD HH:mm:ss"); + // + //var e = moment(w.start).add(d,'seconds').format("YYYY-MM-DD HH:mm:ss"); + + var e = moment.tz(w.end, NVRDataModel.getTimeZoneNow()).tz(moment.tz.guess()); + + console.log ("Start="+s+" End="+e); + $timeout (function() {timeline.setWindow(s,e);},1000);*/ + + } + + lastTimeForEvent = moment().tz(NVRDataModel.getLogin().useLocalTimeZone ? NVRDataModel.getLocalTimeZoneNow() : NVRDataModel.getTimeZoneNow()); + updateInterval = $interval(function() + { + processNewEvents(); + }.bind(this), 10 * 1000); + + $ionicLoading.hide(); + $scope.graphLoaded = true; + NVRDataModel.debug("graph loaded: " + $scope.graphLoaded); + $scope.navControls = false; + var dblclick = false; + + // we don't really need this anymore - as we have an interval timer + // ticking away + + // this is called for each tick the bar moves + // speed moves depending on zoom factor + // + /* timeline.on('currentTimeTick', function() { + + if ($scope.follow.time) { + + } + + + });*/ + + timeline.on('rangechanged', function(s) + { + ///console.log ("Range Changed:"+JSON.stringify(s)); + if (s.byUser) + { + + var w = timeline.getWindow(); + //console.log ("start:"+w.start+" end:"+w.end); + var a = moment(w.start); + var b = moment(w.end); + var d = b.diff(a, 'seconds'); + var ld = NVRDataModel.getLogin(); + ld.timelineScale = d; + NVRDataModel.setLogin(ld); + //console.log ("Stored user scale of "+d+" seconds"); + } + + }); + + timeline.on('click', function(prop) + { + + $timeout(function() + { + if (dblclick) + { + //console.log ("IGNORING CLICK AS DBL CLICK"); + $timeout(function() + { + dblclick = false; + }, 400); + return; + } + //console.log ("CLICK"); + //console.log ("I GOT " + JSON.stringify(prop)); + // console.log ("EVENT IS " + JSON.stringify(properties.event)); + //var properties = timeline.getEventProperties(prop); + // console.log ( "I GOT " + properties); + var itm = prop.item; + //console.log ("ITEM CLICKED " + itm); + if (itm && !isNaN(itm)) + { + NVRDataModel.debug("TimelineCtrl/drawGraph:You clicked on item " + itm); + var item = graphData.get(itm); + NVRDataModel.debug("TimelineCtrl/drawGraph: clicked item details:" + JSON.stringify(item)); + showEvent(item.myevent); + + } + else + { + NVRDataModel.debug("exact match not found, guessing item with co-ordinates X=" + prop.x + " group=" + prop.group); + if (prop.group) + { + var visible = timeline.getVisibleItems(); + NVRDataModel.debug("Visible items=" + JSON.stringify(visible)); + var closestItem = null; + var minDist = 99999; + var _item; + for (var x = 0; x < visible.length; x++) + { + _item = timeline.itemSet.items[x]; + if (_item.data.group == prop.group) + { + if (Math.abs(_item.left - prop.x) < minDist) + { + closestItem = _item; + minDist = Math.abs(_item.left - prop.x); + NVRDataModel.debug("Temporary closest " + _item.left); + //console.log (_item); + } + } + + } + + if (closestItem != null) + { + NVRDataModel.log("Closest item " + closestItem.left + " group: " + closestItem.data.group); + showEvent(closestItem.data.myevent); + } + else + { + NVRDataModel.log("Did not find a visible item match"); + } + } + else // no group row tapped, do nothing + { + + /*$ionicLoading.show({ + template: "", + animation: 'fade-in', + showBackdrop: true, + maxWidth: 200, + showDelay: 0, + duration: 1500, + });*/ + } + // console.log("Zoomed out too far to playback events"); + } + }, 400); + + }); + + timeline.on('doubleClick', function(prop) + { + //console.log ("DOUBLE"); + dblclick = true; + var itm = prop.item; + //console.log ("ITEM CLICKED " + itm); + if (itm && !isNaN(itm)) + { + NVRDataModel.debug("TimelineCtrl/drawGraph:You clicked on item " + itm); + var item = graphData.get(itm); + NVRDataModel.debug("TimelineCtrl/drawGraph: clicked item details:" + JSON.stringify(item)); + eventDetails(item.myevent); + + } + else + { + + NVRDataModel.debug("exact match not found, guessing item with co-ordinates X=" + prop.x + " group=" + prop.group); + if (prop.group) + { + var visible = timeline.getVisibleItems(); + NVRDataModel.debug("Visible items=" + JSON.stringify(visible)); + var closestItem = null; + var minDist = 99999; + var _item; + for (var x = 0; x < visible.length; x++) + { + _item = timeline.itemSet.items[x]; + if (_item.data.group == prop.group) + { + if (Math.abs(_item.left - prop.x) < minDist) + { + closestItem = _item; + minDist = Math.abs(_item.left - prop.x); + NVRDataModel.debug("Temporary closest " + _item.left); + //console.log (_item); + } + } + + } + NVRDataModel.log("Closest item " + closestItem.left + " group: " + closestItem.data.group); + if (closestItem != null) + { + NVRDataModel.log("Closest item " + closestItem.left + " group: " + closestItem.data.group); + showEvent(closestItem.data.myevent); + } + else + { + NVRDataModel.log("Did not find a visible item match"); + } + } + + // console.log("Zoomed out too far to playback events"); + } + + }); + }, + function(error) + { + NVRDataModel.displayBanner('error', 'Timeline error', 'Please try again'); + + } + + ); // get Events + }); + } + + $scope.radialMenuOptions = { + content: '', + //size: 'small', + + background: '#982112', + isOpen: true, + toggleOnClick: false, + button: + { + cssClass: 'fa fa-compress fa-2x', + size: 'small', + onclick: function() + { + //console.log("fitting"); + timeline.fit(); + } + }, + items: [ + { + content: '', + cssClass: 'fa fa-minus-circle', + empty: false, + onclick: function() + { + //zoom(0.2); + timeline.zoomOut(0.2); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: true, + onclick: function() { + + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: false, + + onclick: function() + { + + move(0.2); + } + }, + { + content: 'D', + empty: true, + + onclick: function() + { + // console.log('About'); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: true, + onclick: function() { + + } + }, + + { + content: '', + cssClass: 'fa fa-plus-circle', + empty: false, + onclick: function() + { + + //zoom(-0.2); + timeline.zoomIn(0.2); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: true, + onclick: function() { + + } + }, + + { + content: 'H', + empty: true, + onclick: function() + { + // console.log('About'); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: false, + onclick: function() + { + move(-0.2); + } + }, + + { + content: '', + cssClass: 'fa fa-chevron-circle-up', + empty: true, + onclick: function() { + + } + }, + + { + content: 'K', + empty: true, + onclick: function() + { + //console.log('About'); + } + }, + ] + }; + +}]); diff --git a/www/js/TimelineModalCtrl.js b/www/js/TimelineModalCtrl.js new file mode 100644 index 00000000..506c6efa --- /dev/null +++ b/www/js/TimelineModalCtrl.js @@ -0,0 +1,492 @@ +// Common Controller for the montage view +/* jshint -W041 */ +/* jslint browser: true*/ +/* global saveAs, cordova,StatusBar,angular,console,ionic, moment, vis , Chart, DJS*/ + +angular.module('zmApp.controllers').controller('TimelineModalCtrl', ['$scope', '$rootScope', 'zm', 'NVRDataModel', '$ionicSideMenuDelegate', '$timeout', '$interval', '$ionicModal', '$ionicLoading', '$http', '$state', '$stateParams', '$ionicHistory', '$ionicScrollDelegate', '$q', '$sce', 'carouselUtils', '$ionicPopup', '$translate', function($scope, $rootScope, zm, NVRDataModel, $ionicSideMenuDelegate, $timeout, $interval, $ionicModal, $ionicLoading, $http, $state, $stateParams, $ionicHistory, $ionicScrollDelegate, $q, $sce, carouselUtils, $ionicPopup, $translate) +{ + + var Graph2d; + var tcGraph; + var items; + var groups; + var eventImageDigits = 5; + var cv; + var ctx; + var options; + var data; + var onlyalarm_data; + var current_data; + var current_options; + var btype; + + $scope.graphType = NVRDataModel.getLogin().timelineModalGraphType; + //$scope.graphType = "all"; + $scope.errorDetails = ""; + + //---------------------------------------------------------------- + // Alarm notification handling + //---------------------------------------------------------------- + $scope.handleAlarms = function() + { + $rootScope.isAlarm = !$rootScope.isAlarm; + if (!$rootScope.isAlarm) + { + $rootScope.alarmCount = "0"; + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("events", + { + "id": 0, + "playEvent": false + }, + { + reload: true + }); + return; + } + }; + + //------------------------------------------------------- + // we use this to reload the connkey if authkey changed + //------------------------------------------------------ + + $rootScope.$on("auth-success", function() + { + + NVRDataModel.debug("EventModalCtrl: Re-login detected, resetting everything & re-generating connkey"); + + }); + + $scope.scrollUp = function() + { + //console.log ("SWIPE UP"); + $ionicScrollDelegate.$getByHandle("timeline-modal-delegate").scrollTop(true); + }; + + $scope.scrollDown = function() + { + //console.log ("SWIPE DOWN"); + $ionicScrollDelegate.$getByHandle("timeline-modal-delegate").scrollBottom(true); + }; + + $scope.switchType = function() + { + + if ($scope.graphType == $translate.instant('kGraphAll')) + { + current_data = onlyalarm_data; + $scope.graphType = $translate.instant('kGraphAlarmed'); + NVRDataModel.debug("Alarm array has " + onlyalarm_data.labels.length + " frames"); + btype = 'bar'; + //console.log (JSON.stringify(onlyalarm_data)); + + } + else + { + current_data = data; + // tcGraph.data = + $scope.graphType = $translate.instant('kGraphAll'); + btype = 'line'; + } + + NVRDataModel.log("Switching graph type to " + $scope.graphType); + + var ld = NVRDataModel.getLogin(); + ld.timelineModalGraphType = $scope.graphType; + NVRDataModel.setLogin(ld); + + $timeout(function() + { + + /* + if ($scope.graphType == 'alarmed') + tcGraph.data = data; + else + tcGraph.data = onlyalarm_data; + tcGraph.update();*/ + tcGraph.destroy(); + console.log("GRAPH TYPE IS " + btype); + tcGraph = new Chart(ctx, + { + type: btype, + data: current_data, + options: options + }); + }); + + }; + + //------------------------------------------------------- + // Tapping on a frame shows this image + //------------------------------------------------------ + + $scope.showImage = function(p, r, f, fid, e, imode, id) + { + var img; + console.log("Image Mode " + imode); + if (imode == 'path') + + img = "<img width='100%' ng-src='" + p + "/index.php?view=image&path=" + r + f + "'>"; + else + { + img = "<img width='100%' ng-src='" + p + "/index.php?view=image&fid=" + id + "'>"; + // console.log ("IS MULTISERVER SO IMAGE IS " + img); + } + $rootScope.zmPopup = $ionicPopup.alert( + { + title: 'frame:' + fid + '/Event:' + e, + template: img, + cssClass: 'popup95', + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + }; + + $scope.$on('modal.removed', function(e, m) + { + + if (m.id != 'analyze') + return; + //Graph2d.destroy(); + tcGraph.destroy(); + // Execute action + }); + + //------------------------------------------------------- + // init drawing here + //------------------------------------------------------ + + $scope.$on('modal.shown', function(e, m) + { + + if (m.id != 'analyze') + return; + + $scope.alarm_images = []; + $scope.graphWidth = $rootScope.devWidth - 30; + NVRDataModel.log("Setting init graph width to " + $scope.graphWidth); + $scope.dataReady = false; + + NVRDataModel.getKeyConfigParams(0) + .then(function(data) + { + //console.log ("***GETKEY: " + JSON.stringify(data)); + eventImageDigits = parseInt(data); + NVRDataModel.log("Image padding digits reported as " + eventImageDigits); + }); + + $scope.eventdetails = $translate.instant('kLoading') + "..."; + $scope.mName = NVRDataModel.getMonitorName($scope.event.Event.MonitorId); + $scope.humanizeTime = humanizeTime($scope.event.Event.StartTime); + processEvent(); + //$scope.eventdetails = JSON.stringify($scope.event); + }); + + //------------------------------------------------------- + // okay, really init drawing here + //------------------------------------------------------ + + function processEvent() + { + var eid = $scope.event.Event.Id; + //eid = 22302; + var ld = NVRDataModel.getLogin(); + var apiurl = ld.apiurl + "/events/" + eid + ".json"; + NVRDataModel.log("Getting " + apiurl); + $http.get(apiurl) + .then(function(success) + { + //$scope.eventdetails = JSON.stringify(success); + drawGraphTC(success.data); + }, + function(error) + { + $scope.errorDetails = $translate.instant('kGraphError'); + NVRDataModel.log("Error in timeline frames " + JSON.stringify(error)); + }); + } + + //------------------------------------------------------- + // I was kidding, this is where it really is drawn + // scout's promise + //------------------------------------------------------ + + function drawGraphTC(event) + { + + $scope.eid = event.event.Event.Id; + + $scope.alarm_images = []; + + data = { + labels: [], + datasets: [ + { + label: 'Score', + fill: true, + backgroundColor: 'rgba(89, 171, 227, 1.0)', + borderColor: 'rgba(52, 152, 219, 1.0)', + borderCapStyle: 'butt', + borderJoinStyle: 'miter', + pointBorderColor: "#e74c3c", + pointBackgroundColor: "#e74c3c", + + pointHoverRadius: 10, + pointHoverBackgroundColor: "rgba(249, 105, 14,1.0)", + pointHoverBorderWidth: 1, + tension: 0.1, + + data: [], + frames: [] + }, + + ] + }; + + onlyalarm_data = { + labels: [], + datasets: [ + { + label: 'Score', + backgroundColor: 'rgba(52, 152, 219, 1.0)', + borderColor: 'rgba(52, 152, 219, 1.0)', + hoverBackgroundColor: 'rgba(249, 105, 14,1.0)', + hoverBorderColor: 'rgba(249, 105, 14,1.0)', + data: [], + frames: [] + }, + + ] + }; + + // Chart.js Options + options = { + legend: false, + scales: + { + yAxes: [ + { + ticks: + { + // beginAtZero:true, + min: -1, + }, + }], + xAxes: [ + { + display: false + }] + }, + + responsive: true, + scaleBeginAtZero: true, + scaleShowGridLines: true, + scaleGridLineColor: "rgba(0,0,0,.05)", + scaleGridLineWidth: 1, + + hover: + { + mode: 'single', + onHover: function(obj) + { + if (obj.length > 0) + tapOrHover(obj[0]._index); + } + }, + + //String - A legend template + legendTemplate: '<ul class="tc-chart-js-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>' + }; + + $scope.graphWidth = event.event.Frame.length * 10; + if ($scope.graphWidth < $rootScope.devWidth) + $scope.graphWidth = $rootScope.devWidth; + + // NVRDataModel.log ("Changing graph width to " + $scope.graphWidth); + + for (var i = 0; i < event.event.Frame.length; i++) + { + + data.labels.push(event.event.Frame[i].TimeStamp); + //data.labels.push(' '); + data.datasets[0].data.push(event.event.Frame[i].Score); + data.datasets[0].frames.push( + { + x: event.event.Frame[i].TimeStamp, + y: event.event.Frame[i].Score, + eid: event.event.Event.Id, + fid: event.event.Frame[i].FrameId, + id: event.event.Frame[i].Id, + //group:i, + relativePath: computeRelativePath(event.event), + score: event.event.Frame[i].Score, + fname: padToN(event.event.Frame[i].FrameId, eventImageDigits) + "-capture.jpg", + + }); + + if (event.event.Frame[i].Type == "Alarm") + { + + onlyalarm_data.labels.push(event.event.Frame[i].TimeStamp); + //data.labels.push(' '); + onlyalarm_data.datasets[0].data.push(event.event.Frame[i].Score); + onlyalarm_data.datasets[0].frames.push( + { + x: event.event.Frame[i].TimeStamp, + y: event.event.Frame[i].Score, + eid: event.event.Event.Id, + fid: event.event.Frame[i].FrameId, + //group:i, + relativePath: computeRelativePath(event.event), + score: event.event.Frame[i].Score, + fname: padToN(event.event.Frame[i].FrameId, eventImageDigits) + "-capture.jpg", + id: event.event.Frame[i].Id, + + }); + } + + } + + $scope.dataReady = true; + + cv = document.getElementById("tcchart"); + ctx = cv.getContext("2d"); + + if (NVRDataModel.getLogin().timelineModalGraphType == $translate.instant('kGraphAll')) + { + btype = 'line'; + current_data = data; + } + else + { + btype = 'bar'; + current_data = onlyalarm_data; + } + $timeout(function() + { + tcGraph = new Chart(ctx, + { + type: btype, + data: current_data, + options: options + }); + }); + + cv.onclick = function(e) + { + var b = tcGraph.getElementAtEvent(e); + if (b.length > 0) + { + tapOrHover(b[0]._index); + } + }; + } + + function tapOrHover(ndx) + { + + $timeout(function() + { + + //console.log ("You tapped " + ndx); + $scope.alarm_images = []; + $scope.playbackURL = $scope.event.Event.baseURL; + var items = current_data.datasets[0].frames[ndx]; + $scope.alarm_images.push( + { + relativePath: items.relativePath, + fid: items.fid, + id: items.id, + fname: items.fname, + score: items.score, + time: moment(items.x).format("MMM D," + NVRDataModel.getTimeFormatSec()), + eid: items.eid + }); + }); + + } + + //-------------------------------------------------------- + // utility function + //-------------------------------------------------------- + + function computeRelativePath(event) + { + var relativePath = ""; + var loginData = NVRDataModel.getLogin(); + var str = event.Event.StartTime; + var yy = moment(str).locale('en').format('YY'); + var mm = moment(str).locale('en').format('MM'); + var dd = moment(str).locale('en').format('DD'); + var hh = moment(str).locale('en').format('HH'); + var min = moment(str).locale('en').format('mm'); + var sec = moment(str).locale('en').format('ss'); + relativePath = event.Event.MonitorId + "/" + + yy + "/" + + mm + "/" + + dd + "/" + + hh + "/" + + min + "/" + + sec + "/"; + return relativePath; + + } + + //-------------------------------------------------------- + // utility function + //-------------------------------------------------------- + + function computeBasePath(event) + { + var basePath = ""; + var loginData = NVRDataModel.getLogin(); + var str = event.Event.StartTime; + var yy = moment(str).locale('en').format('YY'); + var mm = moment(str).locale('en').format('MM'); + var dd = moment(str).locale('en').format('DD'); + var hh = moment(str).locale('en').format('HH'); + var min = moment(str).locale('en').format('mm'); + var sec = moment(str).locale('en').format('ss'); + + basePath = loginData.url + "/events/" + + event.Event.MonitorId + "/" + + yy + "/" + + mm + "/" + + dd + "/" + + hh + "/" + + min + "/" + + sec + "/"; + return basePath; + } + + function humanizeTime(str) + { + return moment.tz(str, NVRDataModel.getTimeZoneNow()).fromNow(); + + } + + function padToN(number, digits) + { + + var i; + var stringMax = ""; + var stringLeading = ""; + for (i = 1; i <= digits; i++) + { + stringMax = stringMax + "9"; + if (i != digits) stringLeading = stringLeading + "0"; + } + var numMax = parseInt(stringMax); + + if (number <= numMax) + { + number = (stringLeading + number).slice(-digits); + } + //console.log ("PADTON: returning " + number); + return number; + } + +}]); diff --git a/www/js/WizardCtrl.js b/www/js/WizardCtrl.js new file mode 100644 index 00000000..8f481d2d --- /dev/null +++ b/www/js/WizardCtrl.js @@ -0,0 +1,848 @@ +/* jshint -W041 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console, Masonry, URI */ + +angular.module('zmApp.controllers').controller('zmApp.WizardCtrl', ['$scope', '$rootScope', '$ionicModal', 'NVRDataModel', '$ionicSideMenuDelegate', '$ionicHistory', '$state', '$ionicPopup', 'SecuredPopups', '$http', '$q', 'zm', '$ionicLoading', 'WizardHandler', '$translate', function($scope, $rootScope, $ionicModal, NVRDataModel, $ionicSideMenuDelegate, $ionicHistory, $state, $ionicPopup, SecuredPopups, $http, $q, zm, $ionicLoading, WizardHandler, $translate) +{ + $scope.openMenu = function() + { + $ionicSideMenuDelegate.toggleLeft(); + }; + + //-------------------------------------------------------------------------- + // logs into ZM + //-------------------------------------------------------------------------- + + function login(u, zmu, zmp) + { + var d = $q.defer(); + + $http( + { + method: 'POST', + //withCredentials: true, + url: u, + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + + encodeURIComponent(obj[p])); + var params = str.join("&"); + return params; + }, + + data: + { + username: zmu, + password: zmp, + action: "login", + view: "console" + } + }) + .success(function(data, status, headers) + { + //console.log("LOOKING FOR " + zm.loginScreenString); + //console.log("DATA RECEIVED " + JSON.stringify(data)); + if (data.indexOf(zm.loginScreenString) == -1) + { + + $scope.wizard.loginURL = $scope.wizard.fqportal; + $scope.wizard.portalValidText = $translate.instant('kPortal') + ": " + $scope.wizard.loginURL; + $scope.wizard.portalColor = "#16a085"; + d.resolve(true); + return d.promise; + } + else + { + //console.log("************ERROR"); + $scope.wizard.portalValidText = $translate.instant('kPortalDetectionFailed'); + $scope.wizard.portalColor = "#e74c3c"; + d.reject(false); + return d.promise; + } + }) + .error(function(error) + { + //console.log("************ERROR"); + $scope.wizard.portalValidText = $translate.instant('kPortalDetectionFailed'); + $scope.wizard.portalColor = "#e74c3c"; + d.reject(false); + return d.promise; + + }); + + return d.promise; + + } + + //-------------------------------------------------------------------------- + // we need a monitor ID to do cgi-bin detection - if you don't have + // monitors configured, cgi-bin won't work + //-------------------------------------------------------------------------- + + function getFirstMonitor() + { + var d = $q.defer(); + $http.get($scope.wizard.apiURL + "/monitors.json") + .then(function(success) + { + // console.log("getfirst monitor success: " + JSON.stringify(success)); + if (success.data.monitors.length > 0) + { + var foundMid = -1; + for (var i = 0; i < success.data.monitors.length; i++) + { + if (success.data.monitors[i].Monitor.Function != 'None' && + success.data.monitors[i].Monitor.Enabled == '1') + { + foundMid = success.data.monitors[i].Monitor.Id; + break; + } + } + + if (foundMid != -1) + { + NVRDataModel.debug("zmWizard - getFirstMonitor returned " + foundMid); + d.resolve(foundMid); + return d.promise; + } + else + { + d.reject(false); + return d.promise; + } + + } + else + { + d.reject(false); + return d.promise; + } + }, + function(error) + { + //console.log("getfirst monitor error: " + JSON.stringify(error)); + d.reject(false); + return d.promise; + }); + return d.promise; + } + + //-------------------------------------------------------------------------- + // Utility function - iterates through a list of URLs + // Don't put loginData.reachability here --> we are using this to iterate + // through multiple options - not the same as fallback + //-------------------------------------------------------------------------- + + function findFirstReachableUrl(urls, tail) + { + var d = $q.defer(); + if (urls.length > 0) + { + var t = ""; + if (tail) t = tail; + //$ionicLoading.show({template: 'trying ' + urls[0].server}); + NVRDataModel.log("zmWizard test.." + urls[0] + t); + return $http.get(urls[0] + t).then(function() + { + NVRDataModel.log("Success: on " + urls[0] + t); + //$ionicLoading.hide(); + d.resolve(urls[0]); + return d.promise; + //return urls[0]; + }, function(err) + { + NVRDataModel.log("zmWizard:Failed on " + urls[0] + t + " with error " + JSON.stringify(err) ); + // this is actually a success - I might get empty status + // or something + if (err.status < 300) + { + NVRDataModel.log ("I am taking this as a cgi-bin success - "+urls[0]); + d.resolve(urls[0]); + return d.promise; + } + return findFirstReachableUrl(urls.slice(1), tail); + }); + } + else + { + // $ionicLoading.hide(); + NVRDataModel.log("zmWizard: findFirst returned no success"); + d.reject("No reachable URL"); + return d.promise; + + } + + return d.promise; + + } + + //-------------------------------------------------------------------------- + // removes proto scheme from string + //-------------------------------------------------------------------------- + + function stripProto(u) + { + if (u.indexOf('://') != -1) + return u.substr(u.indexOf('://') + 3); + else + return u; + } + + //-------------------------------------------------------------------------- + // tries to detect cgi-bin + //-------------------------------------------------------------------------- + + function detectcgi() + { + var d = $q.defer(); + var c = URI.parse($scope.wizard.loginURL); + var p1, p2; + p1 = ""; + p2 = ""; + + if (c.userinfo) + p1 = c.userinfo + "@"; + if (c.port) + p2 = ":" + c.port; + + var baseUri = c.scheme + "://" + p1 + c.host + p2; + + NVRDataModel.log("zmWizard CGI: baseURL is " + baseUri); + + var a5 = baseUri + "/zmcgi"; // mageia + var a4 = baseUri + "/cgi-bin/zm"; // another one I found with a CentOS 6 guy + var a3 = baseUri + "/zm/cgi-bin"; // ubuntu/debian + var a2 = baseUri + "/cgi-bin-zm"; //fedora/centos/rhel + var a1 = baseUri + "/cgi-bin"; // doofus + + var urls = [a1, a2, a3, a4, a5]; + + NVRDataModel.getPathZms() // what does ZM have stored in PATH_ZMS? + .then(function(data) + { + // remove zms or nph-zms + var path = data.trim(); + path = path.replace("/nph-zms", ""); + path = path.replace("/zms", ""); + urls.push(baseUri.trim() + path); + NVRDataModel.log("zmWizard: getPathZMS succeeded, adding " + baseUri + path + " to things to try"); + continueCgi(urls); + }, + function(error) + { + NVRDataModel.log("zmWizard: getPathZMS failed, but continuing..."); + continueCgi(urls); + }); + + // Well, PATH_ZMS or not, lets call this function and brute force it + function continueCgi(urls) + { + $ionicLoading.show( + { + template: $translate.instant('kDiscovering') + "...", + noBackdrop: true, + duration: zm.httpTimeout + }); + getFirstMonitor() + .then(function(success) + { + $ionicLoading.hide(); + var tail = "/nph-zms?mode=single&monitor=" + success; + if ($scope.wizard.useauth && $scope.wizard.usezmauth) + { + + var ck = Math.floor(Math.random() * (50000 - 10000 + 1)) + 10000; + NVRDataModel.getAuthKey(success, ck) + .then(function(success) + { + if (success == "") + { + NVRDataModel.log("getAuthKey returned null, so going user=&pwd= way"); + tail += "&user=" + $scope.wizard.zmuser + "&pass=" + $scope.wizard.zmpassword; + } + else + { + tail += success; + } + NVRDataModel.log("auth computed is : " + tail); + proceedwithCgiAfterAuth(urls, tail); + }, + function(error) + { + NVRDataModel.log("Should never come here, getAuthKey doesn't return error"); + + }); + + //console.log ("****CDING " + tail); + } + else // no auth case + { + proceedwithCgiAfterAuth(urls, tail); + } + + function proceedwithCgiAfterAuth(urls, tail) + { + + $ionicLoading.show( + { + template: $translate.instant('kDiscovering') + "...", + noBackdrop: true, + duration: zm.httpTimeout + }); + + findFirstReachableUrl(urls, tail) + .then(function(success) + { + $ionicLoading.hide(); + NVRDataModel.log("Valid cgi-bin found with: " + success); + $scope.wizard.streamingURL = success; + $scope.wizard.streamingValidText = "cgi-bin: " + $scope.wizard.streamingURL; + $scope.wizard.streamingColor = "#16a085"; + d.resolve(true); + return d.promise; + + }, + function(error) + { + $ionicLoading.hide(); + console.log("No cgi-bin found: " + JSON.stringify(error)); + $scope.wizard.streamingValidText = $translate.instant('kPortalCgiBinFailed'); + $scope.wizard.streamingColor = "#e74c3c"; + d.reject(false); + return (d.promise); + }); + } + }, + function(error) + { + $ionicLoading.hide(); + $scope.wizard.streamingValidText = $translate.instant('kPortalCgiBinFailed') + " -" + $translate.instant('kPortalNoMonitorFound'); + $scope.wizard.streamingColor = "#e74c3c"; + d.reject(false); + return (d.promise); + + }); + } + + // https://server/zm/cgi-bin/nph-zms?mode=single&monitor=1&user=admin&pass=cc + + return d.promise; + + } + + //-------------------------------------------------------------------------- + // Finds an appropriate API to use + //-------------------------------------------------------------------------- + + function detectapi() + { + var u = $scope.wizard.loginURL; + var d = $q.defer(); + var api1 = u + "/api"; + var api3 = u + "/zm/api"; + var c = URI.parse(u); + + // lets also try without the path + var api2 = c.scheme + "://"; + if (c.userinfo) api2 += c.userinfo + "@"; + api2 += c.host; + if (c.port) api2 += ":" + c.port; + api2 += "/api"; + + // lets try both /zm/api and /api. What else is there? + var apilist = [api1, api2, api3]; + + findFirstReachableUrl(apilist, '/host/getVersion.json') + .then(function(success) + { + NVRDataModel.log("Valid API response found with:" + success); + $scope.wizard.apiURL = success; + + $scope.wizard.apiValidText = "API: " + $scope.wizard.apiURL; + $scope.wizard.apiColor = "#16a085"; + d.resolve(true); + return d.promise; + }, + function(error) + { + //console.log("No APIs found: " + error); + $scope.wizard.apiValidText = $translate.instant('kPortalAPIFailed'); + $scope.wizard.apiColor = "#e74c3c"; + d.reject(false); + return (d.promise); + }); + + return d.promise; + } + + //-------------------------------------------------------------------------- + // logs out of ZM + //-------------------------------------------------------------------------- + + function logout(u) + { + var d = $q.defer(); + + $http( + { + method: 'POST', + url: u, + headers: + { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + transformRequest: function(obj) + { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + + encodeURIComponent(obj[p])); + var params = str.join("&"); + return params; + }, + + data: + { + action: "logout", + view: "login" + } + }) + .then(function(success) + { + $rootScope.zmCookie = ""; + //console.log("ZMlogout success, cookie removed"); + d.resolve(true); + return d.promise; + }, function(error) + { + //console.log("ZMlogout success"); + d.resolve(true); + return d.promise; + }); + + return d.promise; + + } + + //-------------------------------------------------------------------------- + // clears all status updates in the verify results page - if you + // get back to it + //-------------------------------------------------------------------------- + + $scope.enterResults = function() + { + $scope.portalValidText = ""; + $scope.apiValidateText = ""; + $scope.streamingValidateText = ""; + $scope.wizard.fqportal = ""; + return true; + }; + //-------------------------------------------------------------------------- + // tries to log into the portal and then discover api and cgi-bin + //-------------------------------------------------------------------------- + + function validateData() + { + $rootScope.authSession = 'undefined'; + $rootScope.zmCookie = ''; + + $scope.wizard.portalValidText = ""; + $scope.wizard.apiValidText = ""; + $scope.wizard.streamingValidText = ""; + $scope.wizard.fqportal = ""; + $scope.wizard.loginURL = ""; + $scope.wizard.apiURL = ""; + $scope.wizard.streamingURL = ""; + $scope.wizard.serverName = ""; + + var d = $q.defer(); + + var c = URI.parse($scope.wizard.portalurl); + + $scope.wizard.serverName = c.host; + if (c.port) + $scope.wizard.serverName += "-" + c.port; + + var b = ""; + if ($scope.wizard.useauth && $scope.wizard.usebasicauth) + { + b = $scope.wizard.basicuser + ":" + $scope.wizard.basicpassword + "@"; + //console.log("B=" + b); + } + var u = c.scheme + "://" + b + c.host; + if (c.port) u += ":" + c.port; + if (c.path) u += c.path; + + if (u.slice(-1) == '/') + { + u = u.slice(0, -1); + + } + + $scope.wizard.fqportal = u; + + u = u + '/index.php'; + NVRDataModel.log("Wizard: login url is " + u); + + // now lets login + + var zmu = "x"; + var zmp = "x"; + if ($scope.wizard.usezmauth) + { + zmu = $scope.wizard.zmuser; + zmp = $scope.wizard.zmpassword; + } + + // logout first for the adventurers amongst us who must + // use it even after logging in + NVRDataModel.log("zmWizard: logging out"); + $ionicLoading.show( + { + template: $translate.instant('kCleaningUp') + "...", + noBackdrop: true, + duration: zm.httpTimeout + }); + logout(u) + .then(function(ans) + { + // login now + $ionicLoading.hide(); + NVRDataModel.log("zmWizard: logging in with " + u + " " + zmu); + + // The logic will be: + // Login then do an api detect and cgi-detect together + $ionicLoading.show( + { + template: $translate.instant('kDiscoveringPortal') + "...", + noBackdrop: true, + duration: zm.httpTimeout + }); + login(u, zmu, zmp) + .then(function(success) + { + $ionicLoading.hide(); + NVRDataModel.log("zmWizard: login succeeded"); + + // API Detection + $ionicLoading.show( + { + template: $translate.instant('kDiscoveringAPI') + "...", + noBackdrop: true, + duration: zm.httpTimeout + }); + detectapi() + .then(function(success) + { + $ionicLoading.hide(); + NVRDataModel.log("zmWizard: API succeeded"); + + $ionicLoading.show( + { + template: $translate.instant('kDiscoveringCGI') + "...", + noBackdrop: true, + duration: zm.httpTimeout + }); + // CGI detection + detectcgi() + .then(function(success) + { + $ionicLoading.hide(); + // return true here because we want to progress + return d.resolve(true); + }, + function(error) + { + $ionicLoading.hide(); + // return true here because we want to progress + return d.resolve(true); + }); + }, + function(error) + { + $ionicLoading.hide(); + NVRDataModel.log("zmWizard: api failed"); + + // return true here because we want to progress + return d.resolve(true); + }); + + }, + + // if login failed, don't progress in the wizard + function(error) + { + $ionicLoading.hide(); + NVRDataModel.log("zmWizard: login failed"); + $scope.wizard.portalValidText = $translate.instant('kPortalLoginUnsuccessful'); + $scope.wizard.portalColor = "#e74c3c"; + return d.resolve(true); + + }); + + }); //finally + return d.promise; + } + + //-------------------------------------------------------------------------- + // checks for a protocol + //-------------------------------------------------------------------------- + function checkscheme(url) + { + + if ((!/^(f|ht)tps?:\/\//i.test(url)) && (url != "")) + { + return false; + } + else + return true; + } + + //-------------------------------------------------------------------------- + // exit validator for auth wizard + //-------------------------------------------------------------------------- + + $scope.exitAuth = function() + { + NVRDataModel.log("Wizard: validating auth syntax"); + if ($scope.wizard.useauth) + { + if (!$scope.wizard.usezmauth && !$scope.wizard.usebasicauth) + { + $rootScope.zmPopup = SecuredPopups.show('show', + { + title: $translate.instant('kError'), + template: $translate.instant('kOneAuth'), + buttons: [ + { + text: $translate.instant('kButtonOk') + }] + + }); + return false; + } + if ($scope.wizard.usezmauth) + { + if ((!$scope.wizard.zmuser) || (!$scope.wizard.zmpassword)) + { + $rootScope.zmPopup = SecuredPopups.show('show', + { + title: $translate.instant('kError'), + template: $translate.instant('kValidNameZMAuth'), + buttons: [ + { + text: $translate.instant('kButtonOk') + }] + + }); + return false; + } + } + + if ($scope.wizard.usebasicauth) + { + if ((!$scope.wizard.basicuser) || (!$scope.wizard.basicpassword)) + { + $rootScope.zmPopup = SecuredPopups.show('show', + { + title: $translate.instant('kError'), + template: $translate.instant('kValidNameBasicAuth'), + buttons: [ + { + text: $translate.instant('kButtonOk') + }] + + }); + return false; + } + } + } + // Coming here means we can go to the next step + // load the step + WizardHandler.wizard().next(); + // start discovery; + validateData(); + + }; + + //-------------------------------------------------------------------------- + // validator for portal url wizard + //-------------------------------------------------------------------------- + + $scope.exitPortal = function() + { + NVRDataModel.log("Wizard: validating portal url syntax"); + + if (!$scope.wizard.portalurl) + { + $rootScope.zmPopup = SecuredPopups.show('show', + { + title: $translate.instant('kError'), + template: $translate.instant('kPortalEmpty'), + buttons: [ + { + text: $translate.instant('kButtonOk') + }] + + }); + return false; + } + + if (!checkscheme($scope.wizard.portalurl)) + { + + $scope.portalproto = [ + { + text: "http", + value: "http://" + }, + { + text: "https", + value: "https://" + }]; + $scope.myproto = { + proto: "" + }; + + $rootScope.zmPopup = $ionicPopup.show( + { + title: $translate.instant('kPortalNoProto'), + scope: $scope, + template: $translate.instant('kPortalPleaseSelect') + ': <ion-radio-fix ng-repeat="item in portalproto" ng-value="item.value" ng-model="myproto.proto">{{item.text}}</ion-radio-fix>', + buttons: [ + { + text: $translate.instant('kButtonOk'), + onTap: function(e) + { + NVRDataModel.debug("Protocol selected:" + $scope.myproto.proto); + $scope.wizard.portalurl = $scope.myproto.proto + stripProto($scope.wizard.portalurl); + } + + }] + + }); + return false; + } + + $scope.wizard.portalurl = $scope.wizard.portalurl.toLowerCase().trim(); + + NVRDataModel.log("Wizard: stripped url:" + $scope.wizard.portalurl); + + var c = URI.parse($scope.wizard.portalurl); + + if (!c.scheme) + { + $rootScope.zmPopup = SecuredPopups.show('show', + { + title: $translate.instant('kError'), + template: $translate.instant('kPortalInvalidUrl'), + buttons: [ + { + text: $translate.instant('kButtonOk') + }] + + }); + return false; + } + + if (c.userinfo) // basic auth stuff in here, take it out and put it into the next screen + { + $scope.wizard.useauth = true; + $scope.wizard.usebasicauth = true; + var barray = c.userinfo.split(":", 2); + $scope.wizard.basicuser = barray[0]; + $scope.wizard.basicpassword = barray[1]; + } + + $scope.wizard.portalurl = c.scheme + "://"; + if (c.host) $scope.wizard.portalurl += c.host; + if (c.port) $scope.wizard.portalurl += ":" + c.port; + if (c.path) $scope.wizard.portalurl += c.path; + NVRDataModel.log("Wizard: normalized url:" + $scope.wizard.portalurl); + return true; + }; + + //-------------------------------------------------------------------------- + // part of auth wizard - toggles display of auth components + //-------------------------------------------------------------------------- + $scope.toggleAuth = function() + { + + if (!$scope.wizard.useauth) + { + $scope.wizard.usebasicauth = false; + $scope.wizard.usezmauth = false; + } + }; + + //-------------------------------------------------------------------------- + // global tip toggler for all wizard steps + //-------------------------------------------------------------------------- + $scope.toggleTip = function() + { + $scope.wizard.tipshow = !$scope.wizard.tipshow; + if ($scope.wizard.tipshow) + $scope.wizard.tiptext = $translate.instant('kHideTip'); + else + $scope.wizard.tiptext = $translate.instant('kShowTip'); + }; + + $scope.gotoLoginState = function() + { + $rootScope.wizard = angular.copy($scope.wizard); + $ionicHistory.nextViewOptions( + { + disableBack: true + }); + $state.go("login", + { + "wizard": true + }); + return; + }; + + //-------------------------------------------------------------------------- + // initial + //-------------------------------------------------------------------------- + $scope.$on('$ionicView.beforeEnter', function() + { + //console.log("**VIEW ** Help Ctrl Entered"); + + var monId = -1; + $scope.wizard = { + tipshow: false, + tiptext: $translate.instant('kShowTip'), + useauth: false, + usebasicauth: false, + usezmauth: false, + portalurl: "", + basicuser: "", + basicpassword: "", + zmuser: "", + zmpassword: "", + /////////////////////// + loginURL: "", + apiURL: "", + streamingURL: "", + fqportal: "", + portalValidText: "", + portalColor: "", + apiValidText: "", + apiColor: "", + streamingValidText: "", + streamingColor: "", + serverName: "", + + }; + + }); + +}]); diff --git a/www/js/app.js b/www/js/app.js index 09d5d48b..c859efc3 100644..100755 --- a/www/js/app.js +++ b/www/js/app.js @@ -1,24 +1,2157 @@ -// Ionic Starter App - -// angular.module is a global place for creating, registering and retrieving Angular modules -// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html) -// the 2nd parameter is an array of 'requires' -angular.module('starter', ['ionic']) - -.run(function($ionicPlatform) { - $ionicPlatform.ready(function() { - if(window.cordova && window.cordova.plugins.Keyboard) { - // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard - // for form inputs) - cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); - - // Don't remove this line unless you know what you are doing. It stops the viewport - // from snapping when text inputs are focused. Ionic handles this internally for - // a much nicer keyboard experience. - cordova.plugins.Keyboard.disableScroll(true); - } - if(window.StatusBar) { - StatusBar.styleDefault(); - } - }); -}) +/* jshint -W041, -W093 */ +/* jslint browser: true*/ +/* global cordova,StatusBar,angular,console,alert,PushNotification, moment ,ionic, URI,Packery, ConnectSDK, CryptoJS, ContactFindOptions, localforage,$, Connection, MobileAccessibility, hello */ + +// For desktop versions, this is replaced +// with actual app version from config.xml by the +// ./make_desktop.sh script + +// For mobile versions, I use cordova app version plugin +// to get it at run time + +var appVersion = "0.0.0"; + +// core app start stuff +angular.module('zmApp', [ + 'ionic', + 'ion-datetime-picker', + 'ngIOS9UIWebViewPatch', + 'zmApp.controllers', + 'fileLogger', + 'angular-carousel', + 'angularAwesomeSlider', + 'com.2fdevs.videogular', + 'com.2fdevs.videogular.plugins.controls', + 'com.2fdevs.videogular.plugins.overlayplay', + 'ionic-native-transitions', + 'mgo-angular-wizard', + 'pascalprecht.translate', + 'jett.ionic.scroll.sista', + 'uk.ac.soton.ecs.videogular.plugins.cuepoints', + 'dcbImgFallback' + + ]) + + // ------------------------------------------ + // Various constants central repository + // Feel free to change them as you see fit + //------------------------------------------------ + + .constant('zm', { + minAppVersion: '1.28.107', // if ZM is less than this, the app won't work + recommendedAppVersion: '1.29', + minEventServerVersion: '0.9', + castAppId: 'BA30FB4C', + alarmFlashTimer: 20000, // time to flash alarm + gcmSenderId: '710936220256', + httpTimeout: 15000, + largeHttpTimeout: 60000, + logFile: 'zmNinjaLog.txt', + authoremail: 'pliablepixels+zmNinja@gmail.com', + logFileMaxSize: 20000, // after this limit log gets reset + //loginInterval: 300000, //5m*60s*1000 - ZM auto login after 5 mins + loginInterval: 1800000, //30m*60s*1000 - ZM auto login after 30 mins + + //loginInterval: 30000, + updateCheckInterval: 86400000, // 24 hrs + loadingTimeout: 15000, + slowLoadingTimeout: 60000, + safeMontageLimit: 100, + safeImageQuality: 10, + maxFPS: 30, + defaultFPS: 3, + maxMontageQuality: 70, + defaultMontageQuality: 50, + progressIntervalCheck: 5000, // used for progress indicator on event playback + graphFillColor: 'rgba(151,187,205,0.5)', + graphStrokeColor: 'rgba(151,187,205,0.8)', + graphHighlightFill: 'rgba(0,163,124,0.5)', + graphItemMax: 2000, + graphDesktopItemMax: 2000, + monitorCheckingColor: '#03A9F4', + monitorNotRunningColor: '#F44336', + monitorPendingColor: '#FF9800', + monitorRunningColor: '#4CAF50', + monitorErrorColor: '#795548', + montageScaleFrequency: 300, + eventsListDetailsHeight: 230.0, + eventsListScrubHeight: 330, + loginScreenString: "var currentView = 'login'", // Isn't there a better way? + desktopUrl: "/zm", + desktopApiUrl: "/api/zm", + latestRelease: "https://api.github.com/repos/pliablepixels/zmNinja/releases/latest", + blogUrl: "https://medium.com/zmninja/latest?format=json", + nphSwitchTimer: 3000, + eventHistoryTimer: 5000, + eventPlaybackQuery: 3000, + + packeryTimer: 500, + dbName: 'zmninja', + cipherKey: 'sdf#@#%FSXSA_AR', + minCycleTime: 5, + + eventPlaybackQueryLowBW: 6000, + loginIntervalLowBW: 1800000, //30m login + eventSingleImageQualityLowBW: 70, + monSingleImageQualityLowBW: 70, + montageQualityLowBW: 50, + eventMontageQualityLowBW: 50, + maxGifCount: 60, + maxGifCount2: 100, + maxGifWidth: 800.0, + quantSample: 15, + hashSecret: 'unused at the moment' + + }) + + //http://stackoverflow.com/a/24519069/1361529 + .filter('trusted', ['$sce', function ($sce) { + return function (url) { + return $sce.trustAsResourceUrl(url); + }; + }]) + + + // for events view + .filter('eventListFilter', function (NVRDataModel) { + return function (input) { + var ld = NVRDataModel.getLogin(); + var out = []; + angular.forEach(input, function (item) { + if (item.Event.Archived == '0' || !ld.hideArchived) { + out.push(item); + } + }); + return out; + }; + + }) + + // filter for montage iteration + .filter('onlyEnabled', function () { + + // Create the return function and set the required parameter name to **input** + return function (input) { + + var out = []; + + angular.forEach(input, function (item) { + + if ((item.Monitor.Function != 'None') && + (item.Monitor.Enabled != '0') + ) { + out.push(item); + } + + }); + + return out; + }; + + }) + + // filter for EH iteration + .filter('onlyEnabledAndEventHas', function () { + + // Create the return function and set the required parameter name to **input** + return function (input) { + + var out = []; + + angular.forEach(input, function (item) { + + if ((item.Monitor.Function != 'None') && (item.Monitor.Enabled != '0') && (item.Monitor.eventUrl != 'img/noevent.png') && (item.Monitor.listDisplay != 'noshow')) { + out.push(item); + } + + }); + + return out; + }; + + }) + + + //credit: http://stackoverflow.com/a/23931217/1361529 + .directive('hidepassword', function () { + + var modelSet = function (str) { + + return str; + }; + + var viewSet = function (str) { + //https://github.com/garycourt/uri-js + if (!str) return str; + var c = URI.parse(str); + //if (c.userinfo) c.userinfo="***:***"; + if (c.userinfo) c.userinfo = "\u2022\u2022\u2022:\u2022\u2022\u2022"; + + var ostr = ""; + if (c.scheme) ostr = ostr + c.scheme + "://"; + if (c.userinfo) ostr = ostr + c.userinfo + "@"; + if (c.host) ostr = ostr + c.host; + if (c.port) ostr = ostr + ":" + c.port; + if (c.path) ostr = ostr + c.path; + if (c.query) ostr = ostr + c.query; + if (c.fragment) ostr = ostr + c.fragment; + + return ostr; + }; + + return { + + restrict: 'A', + require: 'ngModel', + link: function (scope, element, attr, ngModel) { + ngModel.$parsers.push(modelSet); + ngModel.$formatters.push(viewSet); + + element.bind('blur', function () { + element.val(viewSet(ngModel.$modelValue)); + }); + element.bind('focus', function () { + element.val(ngModel.$modelValue); + }); + + } + }; + }) + + + + // credit https://gist.github.com/Zren/beaafd64f395e23f4604 + + .directive('mouseWheelScroll', function ($timeout) { + return { + restrict: 'A', + link: function ($scope, $element, $attrs) { + var onMouseWheel, scrollCtrl; + scrollCtrl = $element.controller('$ionicScroll'); + //console.log(scrollCtrl); + if (!scrollCtrl) { + return console.error('mouseWheelScroll must be attached to a $ionicScroll controller.'); + } + onMouseWheel = function (e) { + return scrollCtrl.scrollBy(0, -e.wheelDeltaY, false); + }; + return scrollCtrl.element.addEventListener('wheel', onMouseWheel); + } + }; + }) + + // this can be used to route img-src through interceptors. Works well, but when + // nph-zms streams images it doesn't work as success is never received + // (keeps reading data). Hence not using it now + //credit: http://stackoverflow.com/questions/34958575/intercepting-img-src-via-http-interceptor-as-well-as-not-lose-the-ability-to-kee + .directive('httpSrc', [ + '$http', 'imageLoadingDataShare', 'NVRDataModel', + function ($http, imageLoadingDataShare, NVRDataModel) { + var directive = { + link: postLink, + restrict: 'A' + }; + return directive; + + function postLink(scope, element, attrs) { + //console.log ("HELLO NEW"); + var requestConfig = { + method: 'GET', + //url: attrs.httpSrc, + responseType: 'arraybuffer', + cache: 'true' + }; + + function base64Img(data) { + var arr = new Uint8Array(data); + var raw = ''; + var i, j, subArray, chunk = 5000; + for (i = 0, j = arr.length; i < j; i += chunk) { + subArray = arr.subarray(i, i + chunk); + raw += String.fromCharCode.apply(null, subArray); + } + return btoa(raw); + } + attrs.$observe('httpSrc', function (newValue) { + requestConfig.url = newValue; + //console.log ("requestConfig is " + JSON.stringify(requestConfig)); + imageLoadingDataShare.set(1); + $http(requestConfig) + .success(function (data) { + //console.log ("Inside HTTP after Calling " + requestConfig.url); + //console.log ("data got " + JSON.stringify(data)); + + var b64 = base64Img(data); + attrs.$set('src', "data:image/jpeg;base64," + b64); + imageLoadingDataShare.set(0); + }); + }); + + } + } + ]) + + //------------------------------------------------------------------ + // switch between collection repeat or ng-repeat + //------------------------------------------------------------------- + .directive('repeatsmart', function ($compile, $rootScope) { + return { + restrict: 'A', + priority: 2000, + terminal: true, + link: function (scope, element) { + var repeatDirective = ($rootScope.platformOS == 'desktop') ? 'ng-repeat' : 'collection-repeat'; + //console.log("*********** REPEAT SCROLL IS " + repeatDirective); + + element.attr(repeatDirective, element.attr('repeatsmart')); + element.removeAttr('repeatsmart'); + $compile(element)(scope); + } + }; + }) + + //------------------------------------------------------------------ + // I use this factory to share data between carousel and lazy load + // carousel will not progress autoslide till imageLoading is 0 or -1 + //------------------------------------------------------------------- + .factory('imageLoadingDataShare', function () { + var imageLoading = 0; // 0 = not loading, 1 = loading, -1 = error; + return { + 'set': function (val) { + imageLoading = val; + //console.log ("** IMAGE LOADING **"+val); + }, + 'get': function () { + + return imageLoading; + } + }; + }) + + /*.factory('qHttp', function($q, $http) { + //credit: http://stackoverflow.com/a/29719693 + var queue = $q.when(); + + return function queuedHttp(httpConf) { + var f = function(data) { + return $http(httpConf); + }; + return queue = queue.then(f, f); + }; + })*/ + + //credit: http://stackoverflow.com/a/14468276 + .factory('qHttp', function ($q, $http) { + + var queue = []; + var execNext = function () { + var task = queue[0]; + //console.log ("qHTTP>>> Executing:"+JSON.stringify(task.c)+">>> pending:"+queue.length); + + $http(task.c).then(function (data) { + queue.shift(); + task.d.resolve(data); + if (queue.length > 0) execNext(); + }, function (err) { + queue.shift(); + task.d.reject(err); + if (queue.length > 0) execNext(); + }); + }; + return function (config) { + var d = $q.defer(); + //config.headers.push({'X-qHttp':'enabled'}); + queue.push({ + c: config, + d: d + }); + if (queue.length === 1) { + execNext(); + } + //else + //console.log ("qHTTP>>> Queuing:"+JSON.stringify(config)); + return d.promise; + }; + }) + + //credit: https://github.com/driftyco/ionic/issues/3131 + .factory('SecuredPopups', [ + '$ionicPopup', + '$q', + function ($ionicPopup, $q) { + + var firstDeferred = $q.defer(); + firstDeferred.resolve(); + + var lastPopupPromise = firstDeferred.promise; + + // Change this var to true if you want that popups will automaticly close before opening another + var closeAndOpen = false; + + return { + 'show': function (method, object) { + var deferred = $q.defer(); + var closeMethod = null; + deferred.promise.isOpen = false; + deferred.promise.close = function () { + if (deferred.promise.isOpen && angular.isFunction(closeMethod)) { + closeMethod(); + } + }; + + if (closeAndOpen && lastPopupPromise.isOpen) { + lastPopupPromise.close(); + } + + lastPopupPromise.then(function () { + deferred.promise.isOpen = true; + var popupInstance = $ionicPopup[method](object); + + closeMethod = popupInstance.close; + popupInstance.then(function (res) { + deferred.promise.isOpen = false; + deferred.resolve(res); + }); + }); + + lastPopupPromise = deferred.promise; + + return deferred.promise; + } + }; + } + ]) + + //------------------------------------------------------------------ + // this directive will be called any time an image completes loading + // via img tags where this directive is added (I am using this in + // events and monitor view to show a loader while the image is + // downloading from ZM + //------------------------------------------------------------------ + + .directive('imageonload', function () { + return { + restrict: 'A', + link: function (scope, element, attrs) { + element.bind('load', function () { + //call the function that was passed + scope.$apply(attrs.imageonload); + }); + } + + }; + }) + + //-------------------------------------------------------------------------------------------- + // This directive is adapted from https://github.com/paveisistemas/ionic-image-lazy-load + // I've removed lazyLoad and only made it show a spinner when an image is loading + //-------------------------------------------------------------------------------------------- + .directive('imageSpinnerSrc', ['$document', '$compile', 'imageLoadingDataShare', '$timeout', + function ($document, $compile, imageLoadingDataShare, $timeout) { + return { + restrict: 'A', + scope: { + imageSpinnerBackgroundImage: "@imageSpinnerBackgroundImage" + }, + link: function ($scope, $element, $attributes) { + + /*if ($attributes.imageSpinnerLoader) { + var loader = $compile('<div class="image-loader-container"><ion-spinner class="image-loader" icon="' + $attributes.imageSpinnerLoader + '"></ion-spinner></div>')($scope); + $element.after(loader); + }*/ + + if ($attributes.imageSpinnerLoader) { + var loader = $compile('<div class="image-loader-container"><ion-spinner class="image-loader" icon="' + 'bubbles' + '"></ion-spinner></div>')($scope); + $element.after(loader); + } + imageLoadingDataShare.set(1); + loadImage(); + + $attributes.$observe('imageSpinnerSrc', function (value) { + //console.log ("DIRECTIVE SOURCE CHANGED"); + imageLoadingDataShare.set(1); + loadImage(); + //deregistration(); + + }); + + // show an image-missing image + $element.bind('error', function () { + // console.log ("DIRECTIVE: IMAGE ERROR"); + loader.remove(); + + var url = 'img/novideo.png'; + $element.prop('src', url); + imageLoadingDataShare.set(0); + }); + + function waitForFrame1() { + ionic.DomUtil.requestAnimationFrame( + function () { + imageLoadingDataShare.set(0); + //console.log ("IMAGE LOADED"); + }); + + } + + function loadImage() { + $element.bind("load", function (e) { + if ($attributes.imageSpinnerLoader) { + //console.log ("DIRECTIVE: IMAGE LOADED"); + loader.remove(); + //imageLoadingDataShare.set(0); + //console.log ("rendered"); + + // lets wait for 2 frames for animation + // to render - hoping this will improve tear + // of images + ionic.DomUtil.requestAnimationFrame( + function () { + waitForFrame1(); + }); + + } + }); + + if ($scope.imageSpinnerBackgroundImage == "true") { + var bgImg = new Image(); + bgImg.onload = function () { + if ($attributes.imageSpinnerLoader) { + loader.remove(); + } + // set style attribute on element (it will load image) + if (imageLoadingDataShare.get() != 1) + + $element[0].style.backgroundImage = 'url(' + $attributes.imageSpinnerSrc + ')'; + + //$element[0].style.backgroundImage = 'url(' + 'img/novideo.png'+ ')'; + + }; + + bgImg.src = $attributes.imageSpinnerSrc; + + } else { + $element[0].src = $attributes.imageSpinnerSrc; // set src attribute on element (it will load image) + + } + } + + function isInView() { + return true; + } + + $element.on('$destroy', function () { + + }); + + } + }; + } + ]) + + //------------------------------------------------------------------ + // In Android, HTTP requests seem to get stuck once in a while + // It may be a crosswalk issue. + // To tackle this gracefully, I've set up a global interceptor + // If the HTTP request does not complete in 15 seconds, it cancels + // That way the user can try again, and won't get stuck + // Also remember you need to add it to .config + //------------------------------------------------------------------ + .factory('timeoutHttpIntercept', ['$rootScope', '$q', 'zm', '$injector', function ($rootScope, $q, zm, $injector) { + $rootScope.zmCookie = ""; + //console.log ("HHHHHHHHHHHHHH**************************"); + + return { + + 'request': function (config) { + if (!config) return config; + if (!config.url) return config; + + // NOTE ON TIMEOUTS: As of Oct 10 2016, it seems + // the Http queue often gets messed up when there is a timeout + // and the # of requests are plentiful. I'm going to disable it and see + + // console.log (">>>>"+config.url); + // handle basic auth properly + if (config.url.indexOf("@") > -1) { + //console.log ("HTTP basic auth INTERCEPTOR URL IS " + config.url); + var components = URI.parse(config.url); + // console.log ("Parsed data is " + JSON.stringify(components)); + var credentials = btoa(components.userinfo); + //var authorization = {'Authorization': 'Basic ' + credentials}; + //config.headers.Authorization = 'Basic ' + credentials; + + // console.log ("Full headers: " + JSON.stringify(config.headers)); + + } + + //console.log (">>>>>>>>>>>>> INTERCEPT OBJECT " + JSON.stringify(config)); + + if ($rootScope.zmCookie) { + config.headers.Cookie = "ZMSESSID=" + $rootScope.zmCookie; + } else { + // console.log ("No cookie present in " + config.url); + } + + if ($rootScope.apiAuth) + { + console.log ("********** API AUTH"); + if (config.url.indexOf("/api/") > -1 ) + { + config.url = config.url + "&auth="+$rootScope.authSession; + console.log ("********** API AUTH muggled to:"+config.url); + + } + } + + if ((config.url.indexOf("/api/states/change/") > -1) || + (config.url.indexOf("getDiskPercent.json") > -1) || + (config.url.indexOf("daemonCheck.json") > -1) || + (config.url.indexOf("getLoad.json") > -1)) + + { + + // these can take time, so lets bump up timeout + config.timeout = zm.largeHttpTimeout; + + } else if ((config.url.indexOf("view=view_video") > -1) || + config.url.indexOf(".mp4") > -1) { + // console.log(">>> skipping timers for MP4"); + // put a timeout for zms urls + } else if (config.url.indexOf("zms?") > -1) { + // config.timeout = zm.httpTimeout; + + } + + return config; + }, + + 'response': function (response) { + var cookies = response.headers("Set-Cookie"); + if (cookies != null) { + + var zmSess = cookies.match("ZMSESSID=(.*?);"); + + if (zmSess) { + if (zmSess[1]) { + + // console.log ("***** SETTING COOKIE TO " + zmCookie); + $rootScope.zmCookie = zmSess[1]; + } + } + } + + //console.log ("HTTP response"); + return response; + } + + }; + }]) + + //----------------------------------------------------------------- + // This service automatically checks for new versions every 24 hrs + //------------------------------------------------------------------ + .factory('zmCheckUpdates', function ($interval, $http, zm, $timeout, $localstorage, NVRDataModel, $rootScope, $translate) { + var zmUpdateHandle; + var zmUpdateVersion = ""; + + function start() { + checkUpdate(); + $interval.cancel(zmUpdateHandle); + zmUpdateHandle = $interval(function () { + checkUpdate(); + + }, zm.updateCheckInterval); + + function checkUpdate() { + var lastdateString = NVRDataModel.getLastUpdateCheck(); + var lastdate; + if (!lastdateString) { + + lastdate = moment().subtract(2, 'day'); + + } else { + lastdate = moment(lastdateString); + } + var timdiff = moment().diff(lastdate, 'hours'); + if (timdiff < 24) { + NVRDataModel.log("Checked for update " + timdiff + " hours ago. Not checking again"); + + return; + } + NVRDataModel.log("Checking for new version updates..."); + + $http.get(zm.latestRelease) + .then(function (success) { + + NVRDataModel.setLastUpdateCheck(moment().toISOString()); + // $localstorage.set("lastUpdateCheck", moment().toISOString()); + //console.log ("FULL STRING " + success.data.tag_name); + var res = success.data.tag_name.match("v(.*)"); + zmUpdateVersion = res[1]; + var currentVersion = NVRDataModel.getAppVersion(); + if ($rootScope.platformOS == "desktop") { + zmUpdateVersion = zmUpdateVersion + "D"; + } + //if (NVRDataModel.getAppVersion() != zmUpdateVersion) { + if (NVRDataModel.versionCompare(NVRDataModel.getAppVersion(), zmUpdateVersion) == -1) { + $rootScope.newVersionAvailable = "v" + zmUpdateVersion + " available"; + } else { + $rootScope.newVersionAvailable = ""; + } + NVRDataModel.debug("current version: " + currentVersion + " & available version " + zmUpdateVersion); + //console.log ("Version compare returned: " + NVRDataModel.versionCompare(currentVersion, //zmUpdateVersion)); + // console.log ("Version compare returned: " + NVRDataModel.versionCompare(zmUpdateVersion, currentVersion)); + //console.log ("UPDATE " + zmVersion); + }); + + NVRDataModel.log("Checking for news updates"); + $http.get(zm.blogUrl, { + transformResponse: function (d, h) { + var trunc = "])}while(1);</x>"; + d = d.substr(trunc.length); + return d; + } + }) + + .success(function (datastr) { + + var data = JSON.parse(datastr); + $rootScope.newBlogPost = ""; + if (data.payload.posts.length <= 0) { + $rootScope.newBlogPost = ""; + return; + } + + var lastDate = NVRDataModel.getLatestBlogPostChecked(); + //console.log ("************ BLOG LAST DATE " + lastDate); + if (!lastDate) { + + $rootScope.newBlogPost = "(" + $translate.instant('kNewPost') + ")"; + NVRDataModel.setLatestBlogPostChecked(moment().format("YYYY-MM-DD HH:mm:ss")); + return; + + } + var mLastDate = moment(lastDate); + var mItemDate = moment(data.payload.posts[0].createdAt); + + if (mItemDate.diff(mLastDate, 'seconds') > 0) { + /*console.log ("DIFF IS "+mItemDate.diff(mLastDate, 'seconds')); + console.log ("DIFF mLastDate="+mLastDate); + console.log ("DIFF mItemDate="+mItemDate); + console.log ("FORMAT DIFF mLastDate="+mLastDate.format("YYYY-MM-DD HH:mm:ss") ); + console.log ("FORMAT DIFF mItemDate="+mItemDate.format("YYYY-MM-DD HH:mm:ss") );*/ + + NVRDataModel.debug("New post dated " + mItemDate.format("YYYY-MM-DD HH:mm:ss") + " found, last date checked was " + mLastDate.format("YYYY-MM-DD HH:mm:ss")); + + $rootScope.newBlogPost = "(" + $translate.instant('kNewPost') + ")"; + NVRDataModel.setLatestBlogPostChecked(mItemDate.format("YYYY-MM-DD HH:mm:ss")); + + + + } else { + NVRDataModel.debug("Latest post dated " + mItemDate.format("YYYY-MM-DD HH:mm:ss") + " but you read " + lastDate); + } + + }); + + } + } + + function getLatestUpdateVersion() { + return (zmUpdateVersion == "") ? "(unknown)" : zmUpdateVersion; + } + + return { + start: start, + getLatestUpdateVersion: getLatestUpdateVersion + //stop: stop, + + }; + + }) + + //----------------------------------------------------------------- + // This service automatically logs into ZM at periodic intervals + //------------------------------------------------------------------ + + .factory('zmAutoLogin', function ($interval, NVRDataModel, $http, zm, $browser, $timeout, $q, $rootScope, $ionicLoading, $ionicPopup, $state, $ionicContentBanner, EventServer, $ionicHistory, $translate) { + var zmAutoLoginHandle; + + //------------------------------------------------------------------ + // doLogin() emits this when there is an auth error in the portal + //------------------------------------------------------------------ + + $rootScope.$on("auth-error", function () { + + NVRDataModel.debug("zmAutoLogin: Inside auth-error emit"); + NVRDataModel.displayBanner('error', ['ZoneMinder authentication failed', 'Please check settings']); + + }); + + //------------------------------------------------------------------ + // broadcasted after : + // a) device is ready + // b) language loaded + // c) localforage data loaded + //------------------------------------------------------------------ + + $rootScope.$on("init-complete", function () { + NVRDataModel.log(">>>>>>>>>>>>>>> All init over, going to portal login"); + $ionicHistory.nextViewOptions({ + disableAnimate: true + }); + $state.go("zm-portal-login"); + return; + }); + + //------------------------------------------------------------------ + // doLogin() emits this when our auth credentials work + //------------------------------------------------------------------ + + $rootScope.$on("auth-success", function () { + var contentBannerInstance = $ionicContentBanner.show({ + text: ['ZoneMinder' + $translate.instant('kAuthSuccess')], + interval: 2000, + type: 'info', + transition: 'vertical' + }); + + $timeout(function () { + contentBannerInstance(); + }, 2000); + NVRDataModel.debug("auth-success emit:Successful"); + }); + + $rootScope.getProfileName = function () { + var ld = NVRDataModel.getLogin(); + return (ld.serverName || '(none)'); + }; + + $rootScope.getLocalTimeZone = function () { + return moment.tz.guess(); + }; + + $rootScope.getServerTimeZoneNow = function () { + + return NVRDataModel.getTimeZoneNow(); + + }; + + $rootScope.isTzSupported = function () { + return NVRDataModel.isTzSupported(); + }; + + //------------------------------------------------------------------ + // doLogin() is the function that tries to login to ZM + // it also makes sure we are not back to the same page + // which actually means auth failed, but ZM treats it as a success + //------------------------------------------------------------------ + + function doLogin(str) { + + + var d = $q.defer(); + var ld = NVRDataModel.getLogin(); + + + /*$rootScope.authSession = 'Test'; + $rootScope.apiAuth = true; + d.resolve ("Login Success"); + $rootScope.loggedIntoZm = 1; + $rootScope.$emit('auth-success', 'hash API mode'); + + console.log(">>>>>>>>>>> DO LOGIN"); + if (1) {return (d.promise);}*/ + + + + + NVRDataModel.processFastLogin() + // coming here means login not needed, old login is valid + .then(function (success) { + d.resolve("Login Success due to fast login"); + $rootScope.$emit('auth-success', "fast login mode"); + return d.promise; + }, + + // coming here means login is needed + function (error) { + console.log(">>>>>>>>>>>> FAST FAILED - THIS IS OK"); + + var statename = $ionicHistory.currentStateName(); + + if (statename == "montage-history") { + NVRDataModel.log("Skipping login process as we are in montage history. Re-logging will mess up the stream"); + d.resolve("success"); + return d.promise; + + } + + if ($rootScope.isDownloading) { + NVRDataModel.log("Skipping login process as we are downloading..."); + d.resolve("success"); + return d.promise; + } + + NVRDataModel.debug("Resetting zmCookie..."); + $rootScope.zmCookie = ''; + // first try to login, if it works, good + // else try to do reachability + + console.log(">>>>>>>>>>>> CALLING DO LOGIN"); + proceedWithLogin() + .then(function (success) { + + NVRDataModel.debug("Storing login time as " + moment().toString()); + localforage.setItem("lastLogin", moment().toString()); + d.resolve(success); + return d.promise; + }, + function (error) + // login to main failed, so try others + { + console.log(">>>>>>>>>>>> Failed first login, trying reachability"); + NVRDataModel.getReachableConfig(true) + .then(function (data) { + proceedWithLogin() + .then(function (success) { + d.resolve(success); + return d.promise; + }, + function (error) { + d.reject(error); + return d.promise; + }); + + }, + function (error) { + d.reject(error); + return d.promise; + }); + + }); + + return d.promise; + + function proceedWithLogin() { + // recompute rand anyway so even if you don't have auth + // your stream should not get frozen + $rootScope.rand = Math.floor((Math.random() * 100000) + 1); + $rootScope.modalRand = Math.floor((Math.random() * 100000) + 1); + + // console.log ("***** STATENAME IS " + statename); + + var d = $q.defer(); + var ld = NVRDataModel.getLogin(); + NVRDataModel.log("zmAutologin called"); + + + // This is a good time to check if auth is used :-p + if (!ld.isUseAuth) { + NVRDataModel.log("Auth is disabled!"); + d.resolve("Login Success"); + + $rootScope.$emit('auth-success', 'no auth'); + return (d.promise); + + } + + if (str) { + $ionicLoading.show({ + template: str, + noBackdrop: true, + duration: zm.httpTimeout + }); + } + + + + + + + console.log(">>>>>>>>>>>>>> ISRECAPTCHA"); + + NVRDataModel.isReCaptcha() + .then(function (result) { + if (result == true) { + $ionicLoading.hide(); + NVRDataModel.displayBanner('error', ['reCaptcha must be disabled', ], "", 8000); + var alertPopup = $ionicPopup.alert({ + title: 'reCaptcha enabled', + template: $translate.instant('kRecaptcha'), + okText: $translate.instant('kButtonOk'), + cancelText: $translate.instant('kButtonCancel'), + }); + + // close it after 5 seconds + $timeout(function () { + + alertPopup.close(); + }, 5000); + + d.reject("Error-disable recaptcha"); + return (d.promise); + } + + }); + + var loginData = NVRDataModel.getLogin(); + console.log(">>>>>>>>>>>>>> PARALLEL POST WITH RECAPTCHA TO " + loginData.url); + + /* console.log ("-----------------------SECRET IS "+zm.hashSecret); + $http.get (ld.apiurl+'/host/remoteIp.json') + .then (function (data) { + $ionicLoading.hide(); + var ip = (data.data.auth_hash_ip) ? data.data.remote_ip: ""; + var composite = zm.hashSecret + ld.username + ld.password + ip + data.data.time_frag; + var hash = CryptoJS.MD5(composite); + console.log ("MD5 HASH IS "+hash); + $rootScope.authSession = hash; + d.resolve ("Login Success"); + $rootScope.loggedIntoZm = 1; + $rootScope.$emit('auth-success', data); + + + //ZM_AUTH_HASH_SECRET.$user['Username'].$user['Password'].$remoteAddr.$time[2].$time[3].$time[4].$time[5] + //$rootScope.authSession + // data.data.remote_ip + // data.data.is_auth + + console.log (JSON.stringify(data)); + }, + function (error) { + $ionicLoading.hide(); + console.log (JSON.stringify(error)); + $rootScope.authSession = ""; + d.reject ("Login Error"); + $rootScope.loggedIntoZm = 1; + $rootScope.$emit('auth-error', "incorrect credentials"); + } + + ); + + return (d.promise); + + console.log ("*****************NEVER HERE***********"); + */ + var hDelay = loginData.enableSlowLoading ? zm.largeHttpTimeout : zm.httpTimeout; + //NVRDataModel.debug ("*** AUTH LOGIN URL IS " + loginData.url); + $http({ + + method: 'POST', + timeout: hDelay, + //withCredentials: true, + url: loginData.url + '/index.php', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + transformRequest: function (obj) { + var str = []; + for (var p in obj) + str.push(encodeURIComponent(p) + "=" + + encodeURIComponent(obj[p])); + var params = str.join("&"); + return params; + }, + + data: { + username: loginData.username, + password: loginData.password, + action: "login", + view: "console" + } + }) + .success(function (data, status, headers) { + console.log(">>>>>>>>>>>>>> PARALLEL POST SUCCESS"); + $ionicLoading.hide(); + + // Coming here does not mean success + // it could also be a bad login, but + // ZM returns you to login.php and returns 200 OK + // so we will check if the data has + // <title>ZM - Login</title> -- it it does then its the login page + + if (data.indexOf(zm.loginScreenString) == -1) { + //eventServer.start(); + $rootScope.loggedIntoZm = 1; + + NVRDataModel.log("zmAutologin successfully logged into Zoneminder"); + + d.resolve("Login Success"); + + $rootScope.$emit('auth-success', data); + + } else // this means login error + { + $rootScope.loggedIntoZm = -1; + //console.log("**** ZM Login FAILED"); + NVRDataModel.log("zmAutologin Error: Bad Credentials ", "error"); + $rootScope.$emit('auth-error', "incorrect credentials"); + + d.reject("Login Error"); + return (d.promise); + } + + // Now go ahead and re-get auth key + // if login was a success + $rootScope.authSession = "undefined"; + var ld = NVRDataModel.getLogin(); + NVRDataModel.getAuthKey($rootScope.validMonitorId) + .then(function (success) { + + //console.log(success); + $rootScope.authSession = success; + NVRDataModel.log("Stream authentication construction: " + + $rootScope.authSession); + + }, + function (error) { + //console.log(error); + + NVRDataModel.log("Modal: Error returned Stream authentication construction. Retaining old value of: " + $rootScope.authSession); + NVRDataModel.debug("Error was: " + JSON.stringify(error)); + }); + + return (d.promise); + + }) + .error(function (error, status) { + + console.log(">>>>>>>>>>>>>> PARALLEL POST ERROR"); + $ionicLoading.hide(); + + //console.log("**** ZM Login FAILED"); + + // FIXME: Is this sometimes results in null + + NVRDataModel.log("zmAutologin Error " + JSON.stringify(error) + " and status " + status); + // bad urls etc come here + $rootScope.loggedIntoZm = -1; + $rootScope.$emit('auth-error', error); + + d.reject("Login Error"); + return d.promise; + }); + return d.promise; + } + }); + return d.promise; + + } + + function start() { + var ld = NVRDataModel.getLogin(); + // lets keep this timer irrespective of auth or no auth + $rootScope.loggedIntoZm = 0; + $interval.cancel(zmAutoLoginHandle); + //doLogin(); + zmAutoLoginHandle = $interval(function () { + doLogin(""); + + }, zm.loginInterval); // Auto login every 5 minutes + // PHP timeout is around 10 minutes + // should be ok? + + } + + function stop() { + var ld = NVRDataModel.getLogin(); + + $interval.cancel(zmAutoLoginHandle); + $rootScope.loggedIntoZm = 0; + NVRDataModel.log("Cancelling zmAutologin timer"); + + } + + return { + start: start, + stop: stop, + doLogin: doLogin + }; + }) + + //==================================================================== + // First run in ionic + //==================================================================== + + .run(function ($ionicPlatform, $ionicPopup, $rootScope, zm, $state, $stateParams, NVRDataModel, $cordovaSplashscreen, $http, $interval, zmAutoLogin, zmCheckUpdates, $fileLogger, $timeout, $ionicHistory, $window, $ionicSideMenuDelegate, EventServer, $ionicContentBanner, $ionicLoading, $ionicNativeTransitions, $translate, $localstorage) { + + $rootScope.appName = "zmNinja"; + $rootScope.zmGlobalCookie = ""; + $rootScope.isEventFilterOn = false; + $rootScope.fromDate = ""; + $rootScope.fromTime = ""; + $rootScope.toDate = ""; + $rootScope.toTime = ""; + $rootScope.fromString = ""; + $rootScope.toString = ""; + $rootScope.loggedIntoZm = 0; + $rootScope.apnsToken = ''; + $rootScope.tappedNotification = 0; + $rootScope.tappedMid = 0; + //var eventsToDisplay=[]; + $rootScope.alarmCount = "0"; + $rootScope.platformOS = "desktop"; + $rootScope.currentServerGroup = "defaultServer"; + $rootScope.validMonitorId = ""; + $rootScope.newVersionAvailable = ""; + $rootScope.userCancelledAuth = false; + $rootScope.online = true; + $rootScope.showBlog = false; + $rootScope.newBlogPost = ""; + $rootScope.apiVersion = ""; + + // only for android + $rootScope.exitApp = function () { + NVRDataModel.log("user exited app"); + + ionic.Platform.exitApp(); + }; + + // This is a global exception interceptor + $rootScope.exceptionMessage = function (error) { + NVRDataModel.debug("**EXCEPTION**" + error.reason + " caused by " + error.cause); + }; + + window.addEventListener('beforeunload', function (ev) { + + if ($rootScope.platformOS != 'desktop') { + ev.returnValue = "true"; + return; + } + + localforage.setItem('last-desktop-state', { + 'name': $ionicHistory.currentView().stateName, + 'params': $ionicHistory.currentView().stateParams + }).then(function () { + return localforage.getItem('last-desktop-state'); + }).then(function (value) { + ev.returnValue = "true"; + }).catch(function (err) { + ev.returnValue = "true"; + }); + + }); + + // register callbacks for online/offline + // lets see if it really works + $rootScope.online = navigator.onLine; + + $window.addEventListener("offline", function () { + $rootScope.$apply(function () { + $rootScope.online = false; + NVRDataModel.log("Your network went offline"); + + //$rootScope.$emit('network-change', "offline"); + + }); + }, false); + + $window.addEventListener("online", function () { + $rootScope.$apply(function () { + $rootScope.online = true; + + $timeout(function () { + var networkState = navigator.connection.type; + NVRDataModel.debug("Detected network type as: " + networkState); + var strState = NVRDataModel.getBandwidth(); + NVRDataModel.debug("getBandwidth() normalized it as: " + strState); + $rootScope.runMode = strState; + if ((NVRDataModel.getLogin().autoSwitchBandwidth == true) && + (NVRDataModel.getLogin().enableLowBandwidth == true)) { + NVRDataModel.debug("Setting app state to: " + strState); + $rootScope.$emit('bandwidth-change', strState); + } else { + NVRDataModel.debug("Not changing bandwidth state, as auto change is not on"); + } + + }, 1000); // need a time gap, seems network type registers late + + NVRDataModel.log("Your network is online, re-authenticating"); + zmAutoLogin.doLogin($translate.instant('kReAuthenticating')); + + }); + }, false); + + // This code takes care of trapping the Android back button + // and takes it to the menu. + //console.log (">>>>>>>>>>>>>>>>>>BACK BUTTON REGISTERED"); + $ionicPlatform.registerBackButtonAction(function (e) { + e.preventDefault(); + //console.log ("******** back called with isOpenLeft: " + $ionicSideMenuDelegate.isOpenLeft()); + if (!$ionicSideMenuDelegate.isOpenLeft()) { + $ionicSideMenuDelegate.toggleLeft(); + //console.log("Status of SIDE MENU IS : " + $ionicSideMenuDelegate.isOpen()); + } else { + navigator.app.exitApp(); + } + }, 501); + + // this works reliably on both Android and iOS. The "onorientation" seems to reverse w/h in Android. Go figure. + // http://stackoverflow.com/questions/1649086/detect-rotation-of-android-phone-in-the-browser-with-javascript + + var checkOrientation = function () { + var pixelRatio = window.devicePixelRatio || 1; + $rootScope.devWidth = ((window.innerWidth > 0) ? window.innerWidth : screen.width); + $rootScope.devHeight = ((window.innerHeight > 0) ? window.innerHeight : screen.height); + //console.log("********NEW Computed Dev Width & Height as" + $rootScope.devWidth + "*" + $rootScope.devHeight); + + }; + + window.addEventListener("resize", checkOrientation, false); + + // we come here when a user forcibly cancels portal auth + // useful when you know your auth won't succeed and you need to + // switch to another server + $rootScope.cancelAuth = function () { + $ionicLoading.hide(); + NVRDataModel.log("User cancelled login"); + $ionicHistory.nextViewOptions({ + disableAnimate: true, + disableBack: true + }); + $rootScope.userCancelledAuth = true; + window.stop(); + + //console.log ("inside cancelAuth , calling wizard"); + $state.go("login", { + "wizard": false + }); + return; + }; + + //--------------------------------------------------------------------------- + // authorize state transitions + //---------------------------------------------------------------------------- + + $rootScope.$on('$stateChangeStart', function (event, toState, toParams) { + var requireLogin = toState.data.requireLogin; + + if ($rootScope.apiValid == false && toState.name != 'invalidapi' && toState.data.requireLogin == true) { + event.preventDefault(); + $state.transitionTo('invalidapi'); + return; + + } + + if (NVRDataModel.isLoggedIn() || toState.data.requireLogin == false) { + //console.log("State transition is authorized"); + + return; + } else { + NVRDataModel.log("In Auth State trans: Not logged in, requested to go to " + JSON.stringify(toState)); + // event.preventDefault(); + // + + $state.transitionTo('login'); + + } + + if (requireLogin) { + + $ionicPopup.alert({ + title: $translate.instant('kCredentialsTitle'), + template: $translate.instant('kCredentialsBody') + }); + // for whatever reason, .go was resulting in digest loops. + // if you don't prevent, states will stack + event.preventDefault(); + $state.transitionTo('login'); + return; + } + + return; + + }); + + // credit http://stackoverflow.com/a/2091331/1361529 + function getQueryVariable(query, variable) { + var vars = query.split('&'); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split('='); + if (decodeURIComponent(pair[0]) == variable) { + return decodeURIComponent(pair[1]); + } + } + return ""; + //console.log('Query variable %s not found', variable); + } + + //--------------------------------------------------------------------- + // called when device is ready + //--------------------------------------------------------------------- + + function getTextZoomCallback(tz) { + $rootScope.textScaleFactor = parseFloat(tz + "%") / 100.0; + NVRDataModel.debug("text zoom factor is " + $rootScope.textScaleFactor); + } + + $ionicPlatform.ready(function () { + + + + // handles URL launches + // if you just launch zmninja:// then it will honor the settings in "tap screen" -> events or montage + // if you launch with zmninja://<mid> it will take you to live view for that mid + window.handleOpenURL = function (url) { + $rootScope.tappedNotification = 1; + $rootScope.tappedMid = 0; + var c = URI.parse(url); + //NVRDataModel.log ("***********launched with "+ JSON.stringify(c)); + if (c.query) { + var qm = getQueryVariable(c.query, "mid"); + if (qm) $rootScope.tappedMid = parseInt(qm); + NVRDataModel.log("external URL called with MID=" + $rootScope.tappedMid); + //console.log (">>>>>>>>> EID="+getQueryVariable(c.query, "eid")); + + } + + + + }; + + $rootScope.textScaleFactor = 1.0; + $rootScope.apiValid = false; + + $rootScope.db = null; + $rootScope.runMode = NVRDataModel.getBandwidth(); + + $rootScope.platformOS = "desktop"; + NVRDataModel.log("Device is ready"); + + + // var ld = NVRDataModel.getLogin(); + if ($ionicPlatform.is('ios')) + $rootScope.platformOS = "ios"; + if ($ionicPlatform.is('android')) + $rootScope.platformOS = "android"; + + NVRDataModel.log("You are running on " + $rootScope.platformOS); + + console.log ("Mobile acc"); + if (window.cordova) + MobileAccessibility.getTextZoom(getTextZoomCallback); + + // $rootScope.lastState = "events"; + //$rootScope.lastStateParam = "0"; + +console.log ("localforage config"); + localforage.config({ + name: zm.dbName + + }); + + var order = []; + + if ($rootScope.platformOS == 'ios') { + order = [window.cordovaSQLiteDriver._driver, + localforage.INDEXEDDB, + localforage.WEBSQL, + localforage.LOCALSTORAGE + ]; + } else + + { + // don't do SQL for non IOS - seems to hang? + order = [ + + localforage.INDEXEDDB, + localforage.WEBSQL, + localforage.LOCALSTORAGE, + ]; + + } + +console.log ("forage driver"); + localforage.defineDriver(window.cordovaSQLiteDriver).then(function () { + return localforage.setDriver( + // Try setting cordovaSQLiteDriver if available, + order + + ); + }).then(function () { + // this should alert "cordovaSQLiteDriver" when in an emulator or a device + NVRDataModel.log("localforage driver for storage:" + localforage.driver()); + + // Now lets import old data if it exists: + var defaultServerName = $localstorage.get("defaultServerName"); + + localforage.getItem("defaultServerName") + .then(function (val) { + // console.log (">>>> localforage reported defaultServerName as " + val); + // if neither, we are in first use, mates! + if (!val && !defaultServerName) { + continueInitialInit(); + /* NVRDataModel.debug ("Neither localstorage or forage - First use, showing warm and fuzzy..."); + $ionicHistory.nextViewOptions({ + disableAnimate: true, + disableBack: true + }); + $state.go('first-use');*/ + } else if (!val && defaultServerName) { + NVRDataModel.log(">>>>Importing data from localstorage...."); + + var dsn = defaultServerName; + var dl = $localstorage.get('defaultLang') || 'en'; + var ifu = ($localstorage.get('isFirstUse') == '0' ? false : true); + var luc = $localstorage.get('lastUpdateCheck'); + var lbpc = $localstorage.get('latestBlogPostChecked'); + var sgl = $localstorage.getObject('serverGroupList'); + + NVRDataModel.log(">>>Localstorage data found as below:"); + NVRDataModel.log("server name:" + dsn); + NVRDataModel.log("default lang :" + dl); + NVRDataModel.log("is first use:" + ifu); + NVRDataModel.log("last update check:" + luc); + NVRDataModel.log("latest blog post check:" + lbpc); + NVRDataModel.log("server group list:" + JSON.stringify(sgl)); + + localforage.setItem('defaultLang', dl) + .then(function () { + + NVRDataModel.log(">>>>migrated defaultLang..."); + NVRDataModel.setFirstUse(ifu); + return localforage.setItem('isFirstUse', ifu); + }) + .then(function () { + NVRDataModel.log(">>>>migrated isFirstUse..."); + return localforage.setItem('lastUpdateCheck', ifu); + }) + .then(function () { + NVRDataModel.log(">>>>migrated lastUpdateCheck..."); + return localforage.setItem('latestBlogPostChecked', lbpc); + }) + .then(function () { + NVRDataModel.log(">>>>migrated latestBlogPostChecked..."); + // lets encrypt serverGroupList + NVRDataModel.log("server group list is " + JSON.stringify(sgl)); + var ct = CryptoJS.AES.encrypt(JSON.stringify(sgl), zm.cipherKey); + NVRDataModel.log("encrypted server group list is " + ct); + ct = sgl; + return localforage.setItem('serverGroupList', ct); + }) + .then(function () { + NVRDataModel.log(">>>>migrated serverGroupList..."); + return localforage.setItem('defaultServerName', dsn); + }) + .then(function () { + NVRDataModel.log(">>>>migrated defaultServerName..."); + NVRDataModel.log(">>>>Migrated all values, continuing..."); + //NVRDataModel.migrationComplete(); + continueInitialInit(); + }) + .catch(function (err) { + NVRDataModel.log("Migration error : " + JSON.stringify(err)); + continueInitialInit(); + }); + + } else { + NVRDataModel.log(">>>>No data to import...."); + //NVRDataModel.migrationComplete(); + continueInitialInit(); + } + + }); + + }); + + function continueInitialInit() { + console.log ("continueinit"); + var pixelRatio = window.devicePixelRatio || 1; + $rootScope.devWidth = ((window.innerWidth > 0) ? window.innerWidth : screen.width); + $rootScope.devHeight = ((window.innerHeight > 0) ? window.innerHeight : screen.height); + // for making sure we canuse $state.go with ng-click + // needed for views that use popovers + $rootScope.$state = $state; + $rootScope.$stateParams = $stateParams; + + if (window.cordova && window.cordova.plugins.Keyboard) { + console.log ("no keyboard"); + // cordova.plugins.Keyboard.disableScroll(true); + } + if (window.StatusBar) { + // org.apache.cordova.statusbar required + console.log ("statusbar"); + NVRDataModel.log("Updating statusbar"); + StatusBar.styleDefault(); + //StatusBar.overlaysWebView(false); + StatusBar.backgroundColorByHexString("#2980b9"); + } + + if (window.cordova) { + console.log ("Hiding splash"); + $cordovaSplashscreen.hide(); + + + + console.log ("app version"); + cordova.getAppVersion.getVersionNumber().then(function (version) { + appVersion = version; + NVRDataModel.log("App Version: " + appVersion); + NVRDataModel.setAppVersion(appVersion); + }); + } + + console.log ("file logger"); + $fileLogger.checkFile().then(function (resp) { + if (parseInt(resp.size) > zm.logFileMaxSize) { + console.log ("inside file logger"); + + $fileLogger.deleteLogfile().then(function () { + NVRDataModel.log("Deleting old log file as it exceeds " + zm.logFileMaxSize + " bytes"); + + }); + } + }); + + $fileLogger.setStorageFilename(zm.logFile); + $fileLogger.setTimestampFormat('MMM d, y ' + NVRDataModel.getTimeFormat()); + + if (NVRDataModel.getLogin().disableNative) { + NVRDataModel.log("Disabling native transitions..."); + $ionicNativeTransitions.enable(false); + } else { + NVRDataModel.log("Enabling native transitions..."); + $ionicNativeTransitions.enable(true); + } + // At this stage, DataModel.init is not called yet + // but I do need to know the language + + NVRDataModel.log("Retrieving language before init is called..."); + localforage.getItem("defaultLang") + .then(function (val) { + + var lang = val; + //console.log (">>>>>>>>>>>>>> LANG IS " + val); + + if (lang == undefined || lang == null) { + NVRDataModel.log("No language set, switching to en"); + lang = "en"; + + } else { + NVRDataModel.log("Language stored as:" + lang); + + } + + NVRDataModel.setDefaultLanguage(lang, false) + .then(function (success) { + NVRDataModel.log(">>>>Language to be used:" + $translate.proposedLanguage()); + moment.locale($translate.proposedLanguage()); + + // Remember this is before data Init + // so I need to do a direct forage fetch + localforage.getItem("isFirstUse") + .then(function (val) { + //console.log ("isFirstUse is " + val); + if (val == null || val == true) { + NVRDataModel.log("First time detected"); + $state.go("first-use"); + return; + } else { + continueRestOfInit(); + } + + }); + + }); + }); + } + + function continueRestOfInit() { + + if ($rootScope.platformOS == 'desktop') { + $rootScope.lastState = ""; + $rootScope.lastStateParam = {}; + + localforage.getItem('last-desktop-state') + .then(function (succ) { + // console.log ("FOUND " + JSON.stringify(succ) + ":"+succ); + if (succ) { + $rootScope.lastState = succ.name; + $rootScope.lastStateParam = succ.params; + + } + loadServices(); + }, function (err) { + console.log("ERR " + JSON.stringify(err)); + loadServices(); + }); + } else + + { + + loadServices(); + } + + function loadServices() { + NVRDataModel.log("Language file loaded, continuing with rest"); + NVRDataModel.init(); + + // now do SSL check + //setSSLCerts(); + + EventServer.init(); + zmCheckUpdates.start(); + NVRDataModel.log("Setting up POST LOGIN timer"); + zmAutoLogin.start(); + setupPauseAndResume(); + + + + } + + } + + + function setupPauseAndResume() { + NVRDataModel.log("Setting up pause and resume handler AFTER language is loaded..."); + //--------------------------------------------------------------------------- + // resume handler + //---------------------------------------------------------------------------- + document.addEventListener("resume", function () { + NVRDataModel.log("App is resuming from background"); + $rootScope.isDownloading = false; + var forceDelay = NVRDataModel.getLogin().resumeDelay; + NVRDataModel.log(">>> Resume delayed for " + forceDelay + " ms, to wait for network stack..."); + + $timeout(function () { + var ld = NVRDataModel.getLogin(); + + NVRDataModel.setBackground(false); + // don't animate + $ionicHistory.nextViewOptions({ + disableAnimate: true, + disableBack: true + }); + + // remember the last state so we can + // go back there after auth + if ($ionicHistory.currentView()) { + $rootScope.lastState = $ionicHistory.currentView().stateName; + $rootScope.lastStateParam = + $ionicHistory.currentView().stateParams; + NVRDataModel.debug("Last State recorded:" + + JSON.stringify($ionicHistory.currentView())); + + if ($rootScope.lastState == "zm-portal-login") { + NVRDataModel.debug("Last state was portal-login, so forcing montage"); + $rootScope.lastState = "montage"; + } + + NVRDataModel.debug("going to portal login"); + $ionicHistory.nextViewOptions({ + disableAnimate: true + }); + $state.go("zm-portal-login"); + return; + } else { + $rootScope.lastState = ""; + $rootScope.lastStateParam = ""; + NVRDataModel.debug("reset lastState to null"); + $ionicHistory.nextViewOptions({ + disableAnimate: true + }); + $state.go("zm-portal-login"); + return; + } + + }, forceDelay); + + }, false); + + //--------------------------------------------------------------------------- + // background handler + //---------------------------------------------------------------------------- + document.addEventListener("pause", function () { + NVRDataModel.setBackground(true); + NVRDataModel.setJustResumed(true); // used for window stop + + NVRDataModel.log("ROOT APP:App is going into background"); + + $interval.cancel($rootScope.eventQueryInterval); + $interval.cancel($rootScope.intervalHandle); + + NVRDataModel.log("ROOT APP: Stopping network pull..."); + window.stop(); // dont call stopNetwork - we need to stop here + + var ld = NVRDataModel.getLogin(); + + if (ld.exitOnSleep && $rootScope.platformOS == "android") { + NVRDataModel.log("user exited app"); + ionic.Platform.exitApp(); + } + + zmAutoLogin.stop(); + if ($rootScope.zmPopup) + $rootScope.zmPopup.close(); + + }, false); + + } + + // URL interceptor + + + }); //platformReady + + }) //run + + //------------------------------------------------------------------ + // Route configuration + //------------------------------------------------------------------ + + // My route map connecting menu options to their respective templates and controllers + .config(function ($stateProvider, $urlRouterProvider, $httpProvider, $ionicConfigProvider, $provide, $compileProvider, $ionicNativeTransitionsProvider, $logProvider, $translateProvider) { + + //$logProvider.debugEnabled(false); + //$compileProvider.debugInfoEnabled(false); + + // This is an exception interceptor so it can show up in app logs + // if they occur. I suspect digest and other errors will be useful + // for me to see + //$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|cdvphotolibrary):/); + + $provide.decorator("$exceptionHandler", ['$delegate', '$injector', function ($delegate, $injector) { + return function (exception, cause) { + + var $rootScope = $injector.get("$rootScope"); + $rootScope.exceptionMessage({ + reason: exception, + cause: cause + }); + + $delegate(exception, cause); + + }; + }]); + + // If you do this, Allow Origin can't be * + //$httpProvider.defaults.withCredentials = true; + $httpProvider.interceptors.push('timeoutHttpIntercept'); + $ionicConfigProvider.navBar.alignTitle('center'); + //$ionicConfigProvider.backButton.text('').icon('ion-chevron-left'); + //$ionicConfigProvider.backButton.text('').icon('ion-chevron-left').previousTitleText(false); + // use overflow-scroll=false in ion-content + // removing it here doesn't allow you to enable it per view + // so it messes up scrolldelegate zoom and possibly others + //$ionicConfigProvider.scrolling.jsScrolling(false); + $compileProvider.debugInfoEnabled(false); + + $ionicNativeTransitionsProvider.setDefaultOptions({ + duration: 250, + }); + + $translateProvider.useStaticFilesLoader({ + prefix: 'lang/locale-', + suffix: '.json' + }); + + //$translateProvider.useLocalStorage(); + + $translateProvider.registerAvailableLanguageKeys(['en', 'de', 'es', 'fr', 'it', 'ru', 'ja', 'ko', 'nl', 'pl', 'zh', 'zh_CN', 'zh_TW', 'pt', 'ar', 'hi', 'hu'], { + 'en_*': 'en', + 'de_*': 'de', + 'es_*': 'es', + 'fr_*': 'fr', + 'it_*': 'it', + 'ru_*': 'ru', + 'ja_*': 'ja', + 'ko_*': 'ko', + 'nl_*': 'nl', + 'pt_*': 'pt', + 'pl_*': 'pl', + 'ar_*': 'ar', + 'hi_*': 'hi', + 'hu_*':'hu', + '*': 'en' // must be last + }); + + //$translateProvider.determinePreferredLanguage(); + //$translateProvider.preferredLanguage("en"); + $translateProvider.fallbackLanguage("en"); + $translateProvider.useSanitizeValueStrategy('escape'); + + $stateProvider + .state('app', { + url: '/', + abstract: true, + templateUrl: 'index.html', + cache: false, + + //controller: 'AppCtrl' + }) + + .state('login', { + data: { + requireLogin: false + }, + url: "/login/:wizard", + cache: false, + templateUrl: "templates/login.html", + controller: 'zmApp.LoginCtrl', + + }) + + .state('help', { + data: { + requireLogin: false + }, + url: "/help", + cache: false, + templateUrl: "templates/help.html", + controller: 'zmApp.HelpCtrl', + + }) + + .state('news', { + data: { + requireLogin: false + }, + url: "/news", + cache: false, + templateUrl: "templates/news.html", + controller: 'zmApp.NewsCtrl', + + }) + + .state('monitors', { + data: { + requireLogin: true + }, + resolve: { + message: function (NVRDataModel) { + // console.log("Inside app.montage resolve"); + return NVRDataModel.getMonitors(0); + } + }, + url: "/monitors", + cache: false, + templateUrl: "templates/monitors.html", + controller: 'zmApp.MonitorCtrl', + + }) + + .state('events', { + data: { + requireLogin: true + }, + resolve: { + message: function (NVRDataModel) { + //console.log("Inside app.events resolve"); + return NVRDataModel.getMonitors(0); + } + }, + cache: false, + url: "/events/:id/:playEvent", + templateUrl: "templates/events.html", + controller: 'zmApp.EventCtrl', + + }) + + .state('lowversion', { + data: { + requireLogin: false + }, + + url: "/lowversion/:ver", + cache: false, + templateUrl: "templates/lowversion.html", + controller: 'zmApp.LowVersionCtrl', + + }) + + .state('importantmessage', { + data: { + requireLogin: false + }, + + cache: false, + url: "/importantmessage/:ver", + templateUrl: "templates/important_message.html", + controller: 'zmApp.ImportantMessageCtrl', + + }) + + .state('invalidapi', { + data: { + requireLogin: false + }, + + cache: false, + url: "/invalidapi", + templateUrl: "templates/invalidapi.html", + controller: 'zmApp.InvalidApiCtrl', + + }) + + .state('events-graphs', { + data: { + requireLogin: true + }, + cache: false, + url: "/events-graphs", + templateUrl: "templates/events-graphs.html", + controller: 'zmApp.EventsGraphsCtrl', + + }) + + .state('events-date-time-filter', { + data: { + requireLogin: true + }, + cache: false, + url: "/events-date-time-filter", + templateUrl: "templates/events-date-time-filter.html", + controller: 'zmApp.EventDateTimeFilterCtrl', + + }) + + .state('state', { + data: { + requireLogin: true + }, + cache: false, + url: "/state", + templateUrl: "templates/state.html", + controller: 'zmApp.StateCtrl', + + }) + + .state('devoptions', { + data: { + requireLogin: false + }, + url: "/devoptions", + cache: false, + templateUrl: "templates/devoptions.html", + controller: 'zmApp.DevOptionsCtrl', + }) + + .state('timeline', { + data: { + requireLogin: true + }, + resolve: { + message: function (NVRDataModel) { + //console.log("Inside app.events resolve"); + return NVRDataModel.getMonitors(0); + } + }, + url: "/timeline", + cache: false, + templateUrl: "templates/timeline.html", + controller: 'zmApp.TimelineCtrl', + + }) + + .state('eventserversettings', { + data: { + requireLogin: true + }, + resolve: { + message: function (NVRDataModel) { + return NVRDataModel.getMonitors(0); + } + }, + url: "/eventserversettings", + cache: false, + templateUrl: "templates/eventserversettings.html", + controller: 'zmApp.EventServerSettingsCtrl', + + }) + + .state('log', { + data: { + requireLogin: false + }, + url: "/log", + cache: false, + templateUrl: "templates/log.html", + controller: 'zmApp.LogCtrl', + + }) + + .state('wizard', { + data: { + requireLogin: false + }, + url: "/wizard", + cache: false, + templateUrl: "templates/wizard.html", + controller: 'zmApp.WizardCtrl', + + }) + + .state('zm-portal-login', { + data: { + requireLogin: false + }, + url: "/zm-portal-login", + cache: false, + templateUrl: "templates/zm-portal-login.html", + controller: 'zmApp.PortalLoginCtrl', + nativeTransitions: null // disable for speed + + }) + + .state('first-use', { + data: { + requireLogin: false + }, + url: "/first-use", + cache: false, + templateUrl: "templates/first-use.html", + controller: 'zmApp.FirstUseCtrl', + + }) + + .state('montage-history', { + data: { + requireLogin: true + }, + resolve: { + message: function (NVRDataModel) { + //console.log("Inside app.events resolve"); + return NVRDataModel.getMonitors(0); + } + + }, + cache: false, + url: "/montage-history", + templateUrl: "templates/montage-history.html", + controller: 'zmApp.MontageHistoryCtrl', + params: { + minimal: false, + isRefresh: false + }, + + }) + + .state('montage', { + data: { + requireLogin: true + }, + resolve: { + message: function (NVRDataModel) { + //console.log("Inside app.events resolve"); + return NVRDataModel.getMonitors(0); + } + + }, + url: "/montage", + cache: false, + templateUrl: "templates/montage.html", + controller: 'zmApp.MontageCtrl', + params: { + minimal: false, + isRefresh: false + }, + + }); + + // We are NOT going to default route. Routing to a view will start on + // a broadcast of "init-complete" + + }); //config
\ No newline at end of file diff --git a/www/js/controllers.js b/www/js/controllers.js new file mode 100644 index 00000000..5321384b --- /dev/null +++ b/www/js/controllers.js @@ -0,0 +1,29 @@ +/* jshint -W041 */ +/* jshint browser: true*/ +/* global cordova,StatusBar,angular,console */ + + + +angular.module('zmApp.controllers', ['ionic', 'ionic.utils', 'ngCordova', 'ng-mfb', 'angularCircularNavigation', 'jett.ionic.content.banner', 'ionic-pullup', 'ngWebsocket']) + +.controller('zmApp.BaseController', function ($scope, $ionicSideMenuDelegate, $ionicPlatform, $timeout, $rootScope) { + $scope.openMenu = function () { + $ionicSideMenuDelegate.toggleLeft(); + }; + + $ionicPlatform.registerBackButtonAction(function (event) { + + $ionicSideMenuDelegate.toggleLeft(); + $timeout(function () { + $rootScope.stateofSlide = $ionicSideMenuDelegate.isOpen() + new Date(); + }, 500); + + + }, 100); + + // Added for electron build to stop title propagation + $scope.$on('$ionicView.afterEnter', function (ev, data) { + ev.stopPropagation(); + }); + +});
\ No newline at end of file diff --git a/www/js/ionicUtils.js b/www/js/ionicUtils.js new file mode 100644 index 00000000..c593624c --- /dev/null +++ b/www/js/ionicUtils.js @@ -0,0 +1,32 @@ +/* jshint -W041 */ +/* jshint browser: true*/ +/* global cordova,StatusBar,angular,console */ + +//http://learn.ionicframework.com/formulas/localstorage/ + +angular.module('ionic.utils', []) + +.factory('$localstorage', ['$window', function($window) +{ + return { + + init: function() {}, + + set: function(key, value) + { + $window.localStorage[key] = value; + }, + get: function(key, defaultValue) + { + return $window.localStorage[key] || defaultValue; + }, + setObject: function(key, value) + { + $window.localStorage[key] = JSON.stringify(value); + }, + getObject: function(key) + { + return JSON.parse($window.localStorage[key] || '{}'); + } + }; +}]); |
