From b28028ac4082842143b0f528d6bc539da6ccb419 Mon Sep 17 00:00:00 2001 From: Pliable Pixels Date: Thu, 21 Sep 2017 12:49:18 -0400 Subject: mega changes, including updates and X --- www/js/DataModel.js | 2193 +++++++++++++++++++++++++++ www/js/DevOptionsCtrl.js | 139 ++ www/js/EventCtrl.js | 3002 +++++++++++++++++++++++++++++++++++++ www/js/EventDateTimeFilterCtrl.js | 138 ++ www/js/EventModalCtrl.js | 1873 +++++++++++++++++++++++ www/js/EventServer.js | 584 ++++++++ www/js/EventServerSettingsCtrl.js | 360 +++++ www/js/EventsGraphsCtrl.js | 250 +++ www/js/EventsModalGraphCtrl.js | 409 +++++ www/js/FirstUseCtrl.js | 95 ++ www/js/HelpCtrl.js | 92 ++ www/js/ImportantMessageCtrl.js | 35 + www/js/InvalidApiCtrl.js | 37 + www/js/LogCtrl.js | 315 ++++ www/js/LoginCtrl.js | 860 +++++++++++ www/js/LowVersionCtrl.js | 24 + www/js/MenuController.js | 55 + www/js/MonitorCtrl.js | 550 +++++++ www/js/MonitorModalCtrl.js | 1768 ++++++++++++++++++++++ www/js/MontageCtrl.js | 2024 +++++++++++++++++++++++++ www/js/MontageHistoryCtrl.js | 1496 ++++++++++++++++++ www/js/NewsCtrl.js | 132 ++ www/js/PortalLoginCtrl.js | 454 ++++++ www/js/StateCtrl.js | 410 +++++ www/js/TimelineCtrl.js | 1608 ++++++++++++++++++++ www/js/TimelineModalCtrl.js | 492 ++++++ www/js/WizardCtrl.js | 848 +++++++++++ www/js/app.js | 2181 ++++++++++++++++++++++++++- www/js/controllers.js | 29 + www/js/ionicUtils.js | 32 + 30 files changed, 22461 insertions(+), 24 deletions(-) create mode 100755 www/js/DataModel.js create mode 100644 www/js/DevOptionsCtrl.js create mode 100644 www/js/EventCtrl.js create mode 100644 www/js/EventDateTimeFilterCtrl.js create mode 100644 www/js/EventModalCtrl.js create mode 100644 www/js/EventServer.js create mode 100644 www/js/EventServerSettingsCtrl.js create mode 100644 www/js/EventsGraphsCtrl.js create mode 100644 www/js/EventsModalGraphCtrl.js create mode 100644 www/js/FirstUseCtrl.js create mode 100644 www/js/HelpCtrl.js create mode 100644 www/js/ImportantMessageCtrl.js create mode 100644 www/js/InvalidApiCtrl.js create mode 100644 www/js/LogCtrl.js create mode 100644 www/js/LoginCtrl.js create mode 100644 www/js/LowVersionCtrl.js create mode 100644 www/js/MenuController.js create mode 100644 www/js/MonitorCtrl.js create mode 100644 www/js/MonitorModalCtrl.js create mode 100644 www/js/MontageCtrl.js create mode 100644 www/js/MontageHistoryCtrl.js create mode 100644 www/js/NewsCtrl.js create mode 100644 www/js/PortalLoginCtrl.js create mode 100644 www/js/StateCtrl.js create mode 100644 www/js/TimelineCtrl.js create mode 100644 www/js/TimelineModalCtrl.js create mode 100644 www/js/WizardCtrl.js mode change 100644 => 100755 www/js/app.js create mode 100644 www/js/controllers.js create mode 100644 www/js/ionicUtils.js (limited to 'www/js') 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, ""); + val = val.replace(regex2, ""); + } + $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, ""); + val = val.replace(regex2, ""); + + } + // 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: '
' + $translate.instant('kFrame') + ':{{parray[ndx].frameid}}@{{prettifyTimeSec(parray[ndx].time)}}

', + 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') + ':
' + myFrom + " " + $translate.instant('kTo') + " " + toString + '
' + $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: '' + }); + + 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: "
" + $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 $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: '
Frame: {{slideIndex+1}} / {{slideLastIndex+1}}

', + 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 = '' + $translate.instant('kTapEvents') + ''; + + options += '' + $translate.instant('kTapMontage') + ''; + options += '' + $translate.instant('kTapLiveMonitor') + ''; + + $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 : '
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
' + }; + }; //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: '
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
' + }; + + 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: '
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
' + }; + + $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: ' {{item.text}} ', + + 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, ""); + }*/ + // 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, ""); + } + urlNoProtocol = loginData.streamingurl.replace(/.*?:\/\//, ""); + if (urlNoProtocol != "") + { + var re3 = new RegExp(urlNoProtocol, "g"); + logstring = logstring.replace(re3, ""); + } + + urlNoProtocol = loginData.eventServer.replace(/.*?:\/\//, ""); + if (urlNoProtocol != "") + { + var re4 = new RegExp(urlNoProtocol, "g"); + logstring = logstring.replace(re4, ""); + } + + 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("") + // 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: ' {{item.text}} ', + + 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: 'Enabled {{item.text}} ', + + 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+"
n:"+$scope.zoneArray[i].coords+"--------------------------------------------------
"; + + } + + $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