From 10f5788d747d2f96027c0591fe332b74f9b9aac7 Mon Sep 17 00:00:00 2001 From: PliablePixels Date: Wed, 5 Aug 2015 17:59:26 -0400 Subject: Timeline feature (Experimental) --- www/js/TimelineCtrl.js | 503 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 354 insertions(+), 149 deletions(-) (limited to 'www/js/TimelineCtrl.js') diff --git a/www/js/TimelineCtrl.js b/www/js/TimelineCtrl.js index adeed4a9..3562f2c7 100644 --- a/www/js/TimelineCtrl.js +++ b/www/js/TimelineCtrl.js @@ -4,11 +4,12 @@ /* jslint browser: true*/ /* global vis,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 +// This controller creates a timeline +// It uses the visjs library, but due to performance reasons +// I've disabled pan and zoom and used buttons instead +// also limits # of items to maxItems (currently 200) -angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPlatform', '$scope', 'zm', 'ZMDataModel', '$ionicSideMenuDelegate', '$rootScope', '$http', '$q','message','$state', '$ionicLoading', '$ionicPopover', function ($ionicPlatform, $scope, zm, ZMDataModel, $ionicSideMenuDelegate, $rootScope, $http, $q,message, $state, $ionicLoading, $ionicPopover) { +angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPlatform', '$scope', 'zm', 'ZMDataModel', '$ionicSideMenuDelegate', '$rootScope', '$http', '$q', 'message', '$state', '$ionicLoading', '$ionicPopover', '$ionicScrollDelegate', '$ionicModal', function ($ionicPlatform, $scope, zm, ZMDataModel, $ionicSideMenuDelegate, $rootScope, $http, $q, message, $state, $ionicLoading, $ionicPopover, $ionicScrollDelegate, $ionicModal) { console.log("Inside Timeline controller"); $scope.openMenu = function () { @@ -23,10 +24,232 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla }]; $scope.prettify = function (str) { - return moment(str).format('MMMM Do YYYY, h:mm:ssa'); + return moment(str).format('MMMM Do YYYY, h:mm:ssa'); + }; + + + $scope.calcMsTimer = function (frames, len) { + var myframes, mylen; + myframes = parseFloat(frames); + mylen = parseFloat(len); + // console.log ("frames " + myframes + "length " + mylen); + // console.log ("*** MS COUNT " + (1000.0/(myframes/mylen))); + return (Math.round(1000 / (myframes / mylen))); + }; + + + $scope.move = function (percentage) { + var range = timeline.getWindow(); + var interval = range.end - range.start; + + timeline.setWindow({ + start: range.start.valueOf() - interval * percentage, + end: range.end.valueOf() - interval * percentage + }); + }; + + $scope.zoom = function (percentage) { + var range = timeline.getWindow(); + var interval = range.end - range.start; + + timeline.setWindow({ + start: range.start.valueOf() - interval * percentage, + end: range.end.valueOf() + interval * percentage + }); + }; + + + 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; + } + + + //-------------------------------------------------------- + // To show a modal dialog with the event tapped on in timeline + // FIXME : code repeat from Events + //-------------------------------------------------------- + function openModal(eid, ename, edur, eframes, basepath, relativepath) { + console.log("Open Modal with Base path " + basepath); + $scope.eventName = ename; + $scope.eventId = eid; + $scope.eFramesNum = eframes; + $scope.eventDur = Math.round(edur); + $scope.loginData = ZMDataModel.getLogin(); + $scope.eventBasePath = basepath; + $scope.relativePath = relativepath; + $rootScope.rand = Math.floor(Math.random() * (999999 - 111111 + 1)) + 111111; + + $scope.slider_modal_options = { + from: 1, + to: eframes, + 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; + //console.log("**Resetting range"); + $scope.slides = []; + var i; + for (i = 1; i <= eframes; 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 + var loginData = ZMDataModel.getLogin(); + var myurl = loginData.apiurl + '/events/' + eid + ".json"; + ZMDataModel.zmLog("*** Constructed API for detailed events: " + myurl); + $http.get(myurl) + .success(function (data) { + $scope.FrameArray = data.event.Frame; + // $scope.slider_options.scale=[]; + $scope.slider_modal_options.scale = []; + //$scope.slider_options.modelLabels={2:'X'}; + //$scope.slider_options.dimension="arjun"; + var i; + for (i = 0; i < data.event.Frame.length; i++) { + if (data.event.Frame[i].Type == "Alarm") { + //⬤ + // console.log ("**ALARM AT " + i); + $scope.slider_modal_options.scale.push({ + val: i + 1, + label: ' ' + }); + } else { + //$scope.slider_options.scale.push(' '); + } + + } + + //console.log (JSON.stringify(data)); + }) + .error(function (err) { + ZMDataModel.zmLog("Error retrieving detailed frame API " + JSON.stringify(err)); + }); + + $scope.totalEventTime = Math.round(parseFloat(edur)) - 1; + $scope.currentEventTime = 0; + + ZMDataModel.setAwake(ZMDataModel.getKeepAwake()); + + $ionicModal.fromTemplateUrl('templates/events-modal.html', { + scope: $scope, + animation: 'slide-in-up' + }) + .then(function (modal) { + $scope.modal = modal; + + $ionicLoading.show({ + template: "please wait...", + noBackdrop: true, + duration: 10000 + }); + + $scope.modal.show(); + + var ld = ZMDataModel.getLogin(); + + }); + + } + + //-------------------------------------------------------- + //We need to destroy because we are instantiating + // it on open + //-------------------------------------------------------- + $scope.closeModal = function () { + // $interval.cancel(eventsInterval); + //$interval.cancel(segmentHandle); + console.log("Close & Destroy Modal"); + ZMDataModel.setAwake(false); + if ($scope.modal !== undefined) { + $scope.modal.remove(); + } + + }; + + + //-------------------------------------------------------- + // This function is called by the graph ontapped function + // which in turn calls openModal + //-------------------------------------------------------- + + function showEvent(start, mid, edur, eframes, eid, ename) { + console.log("Event STARTED WITH " + start); + var str = start; + var yy = moment(str).format('YY'); + var mm = moment(str).format('MM'); + var dd = moment(str).format('DD'); + var hh = moment(str).format('HH'); + var min = moment(str).format('mm'); + var sec = moment(str).format('ss'); + var relativepath = + mid + "/" + + yy + "/" + + mm + "/" + + dd + "/" + + hh + "/" + + min + "/" + + sec + "/"; + console.log("PATH IS " + relativepath); + + openModal(eid, ename, edur, eframes, "", relativepath); + + } + + //------------------------------------------------- // Make sure we delete the timeline // This may be redundant as the root view gets @@ -37,27 +260,32 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla //timeline.destroy(); }); + //------------------------------------------------- + // FIXME: shitty hackery -- Im using a rootScope + // to know if you just went to custom range + // and back. Fix this, really. + // So anyway, if you did select a custom range + // then we "go back" to timeline, which is when + // we come here - so make sure we update the + // graph range + //------------------------------------------------- $scope.$on('$ionicView.afterEnter', function () { - console.log ("***AFTER ENTER"); - - if ($rootScope.customTimelineRange) - { - console.log ("***** CUSTOM RANGE"); - if (moment($rootScope.fromString).isValid() && - moment($rootScope.toString).isValid()) - { - console.log ("FROM & TO IS CUSTOM"); - fromDate = $rootScope.fromString; - toDate = $rootScope.toString; - $scope.fromDate = fromDate; - $scope.toDate = toDate; - drawGraph(fromDate, toDate, maxItems); - } - else - { - console.log ("FROM & TO IS CUSTOM INVALID"); + console.log("***AFTER ENTER"); + + if ($rootScope.customTimelineRange) { + console.log("***** CUSTOM RANGE"); + if (moment($rootScope.fromString).isValid() && + moment($rootScope.toString).isValid()) { + console.log("FROM & TO IS CUSTOM"); + fromDate = $rootScope.fromString; + toDate = $rootScope.toString; + $scope.fromDate = fromDate; + $scope.toDate = toDate; + drawGraph(fromDate, toDate, maxItems); + } else { + console.log("FROM & TO IS CUSTOM INVALID"); + } } - } }); //------------------------------------------------- @@ -68,6 +296,35 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla // does not interfere with graph panning $ionicSideMenuDelegate.canDragContent(false); + // FIXME: Timeline awake to avoid graph redrawing + ZMDataModel.setAwake(ZMDataModel.getKeepAwake()); + + $scope.$watch('ionRange.index', function () { + // console.log ("***ION RANGE CHANGED"); + + $scope.mycarousel.index = parseInt($scope.ionRange.index) - 1; + }); + + $scope.$watch('mycarousel.index', function () { + + $scope.ionRange.index = ($scope.mycarousel.index + 1).toString(); + }); + + $scope.mycarousel = { + index: 0 + }; + $scope.ionRange = { + index: 1 + }; + + var eventImageDigits = 5; // failsafe + ZMDataModel.getKeyConfigParams(0) + .then(function (data) { + //console.log ("***GETKEY: " + JSON.stringify(data)); + eventImageDigits = parseInt(data); + ZMDataModel.zmLog("Image padding digits reported as " + eventImageDigits); + }); + // fromDate and toDate will be used to plot the range for the graph // We start in day mode var fromDate = moment().startOf('day').format("YYYY-MM-DD HH:mm:ss"); @@ -78,26 +335,27 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla $scope.fromDate = fromDate; $scope.toDate = toDate; - var maxItems = 200; + var maxItems = 200; // THAT magic # --> 300 and ZM on my m/c cries + $scope.maxItems = maxItems; //flat colors for graph - https://flatuicolors.com var colors = ['#3498db', '#83adb5', '#c7bbc9', '#f39c12', '#bfb5b2', '#e74c3c']; - var container ; + var container; container = angular.element(document.getElementById('visualization')); - var timeline=""; + var timeline = ""; $scope.monitors = message; $ionicPopover.fromTemplateUrl('templates/timeline-popover.html', { - scope: $scope, - }).then(function(popover) { - $scope.popover = popover; - }); + scope: $scope, + }).then(function (popover) { + $scope.popover = popover; + }); - //drawGraph(fromDate, toDate,maxItems); - dummyDrawGraph(fromDate, toDate,maxItems); + drawGraph(fromDate, toDate, maxItems); + //dummyDrawGraph(fromDate, toDate,maxItems); //------------------------------------------------- // Rest graph to sane state after you went @@ -112,42 +370,37 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla // so we can redraw the graph //------------------------------------------------- - $scope.buttonClicked = function(index) - { + $scope.buttonClicked = function (index) { //console.log (index); if (index == 0) //month { - ZMDataModel.zmLog ("Month view"); + ZMDataModel.zmLog("Month view"); $rootScope.customTimelineRange = false; toDate = moment().format("YYYY-MM-DD HH:mm:ss"); - fromDate = moment().subtract(1,'month').startOf('day').format("YYYY-MM-DD HH:mm:ss"); + fromDate = moment().subtract(1, 'month').startOf('day').format("YYYY-MM-DD HH:mm:ss"); $scope.fromDate = fromDate; $scope.toDate = toDate; - drawGraph(fromDate, toDate,maxItems); - } - else if (index == 1) //week + drawGraph(fromDate, toDate, maxItems); + } else if (index == 1) //week { $rootScope.customTimelineRange = false; ZMDataModel.zmLog("Week view"); toDate = moment().format("YYYY-MM-DD HH:mm:ss"); - fromDate = moment().subtract(1,'week').startOf('day').format("YYYY-MM-DD HH:mm:ss"); + fromDate = moment().subtract(1, 'week').startOf('day').format("YYYY-MM-DD HH:mm:ss"); $scope.fromDate = fromDate; $scope.toDate = toDate; - drawGraph(fromDate, toDate,maxItems); - } - else if (index==2) //day + drawGraph(fromDate, toDate, maxItems); + } else if (index == 2) //day { $rootScope.customTimelineRange = false; - ZMDataModel.zmLog ("Day view"); + ZMDataModel.zmLog("Day view"); toDate = moment().format("YYYY-MM-DD HH:mm:ss"); - fromDate = moment().subtract(1,'day').startOf('day').format("YYYY-MM-DD HH:mm:ss"); + fromDate = moment().subtract(1, 'day').startOf('day').format("YYYY-MM-DD HH:mm:ss"); $scope.fromDate = fromDate; $scope.toDate = toDate; - drawGraph(fromDate, toDate,maxItems); - } - - else // custom + drawGraph(fromDate, toDate, maxItems); + } else // custom { $rootScope.customTimelineRange = true; $state.go('events-date-time-filter'); @@ -155,66 +408,8 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla }; - // ------------------------------------------------------ - // Draws a random graph from Vis timeline performance - // ----------------------------------------------------- - function dummyDrawGraph(fromDate, toDate, count) - { - if (timeline) timeline.destroy(); - - var groups = new vis.DataSet([ - {id: 1, content: 'Truck 1'}, - {id: 2, content: 'Truck 2'}, - {id: 3, content: 'Truck 3'}, - {id: 4, content: 'Truck 4'} - ]); - - // create items - var items = new vis.DataSet(); - - var order = 1; - var truck = 1; - for (var j = 0; j < 4; j++) { - var date = new Date(); - for (var i = 0; i < count/4; i++) { - date.setHours(date.getHours() + 4 * (Math.random() < 0.2)); - var start = new Date(date); - - date.setHours(date.getHours() + 2 + Math.floor(Math.random()*4)); - var end = new Date(date); - - items.add({ - id: order, - group: truck, - start: start, - end: end, - content: 'Order ' + order - }); - - order++; - } - truck++; - } - - // specify options - var options = { - stack: false, - start: new Date(), - end: new Date(1000*60*60*24 + (new Date()).valueOf()), - editable: false, - margin: { - item: 10, // minimal margin between items - axis: 5 // minimal margin between items and the axis - }, - orientation: 'top' - }; - timeline = new vis.Timeline(container[0], null, options); - timeline.setGroups(groups); - timeline.setItems(items); - } - - //------------------------------------------------- + //------------------------------------------------- // This function draws the graph // So far struggling with mobile perf // Observations so far: @@ -225,17 +420,16 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla function drawGraph(fromDate, toDate, count) { - $ionicLoading.show({ - template: "Loading graph...", - animation: 'fade-in', - showBackdrop: true, - maxWidth: 200, - showDelay: 0, - duration: zm.loadingTimeout, //specifically for Android - http seems to get stuck at times - }); - - if (timeline) timeline.destroy(); + $ionicLoading.show({ + template: "Loading graph...", + animation: 'fade-in', + showBackdrop: true, + maxWidth: 200, + showDelay: 0, + duration: zm.loadingTimeout, //specifically for Android - http seems to get stuck at times + }); + if (timeline) timeline.destroy(); var groups = new vis.DataSet(); @@ -244,18 +438,16 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla var options = { - // autoResize: false, // true makes it much slower - //configure: true, + editable: false, - // moveable: true, - // zoomable: true, - start:fromDate, - end:toDate, + moveable: false, + zoomable: false, + selectable: true, + start: fromDate, + end: toDate, orientation: 'top', min: fromDate, max: toDate, - //width:'90%', - zoomMin: 1 * 60 * 1000, // 1 min stack: false, format: { @@ -285,14 +477,14 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla // I figure out how many items the server returns per API page // and divide the # of items I want (currently 200) with # of items per page // So iterCount is the # of HTTP calls I need to make - iterCount = Math.round(count/itemsPerPage); + iterCount = Math.round(count / itemsPerPage); console.log("I will make " + iterCount + " HTTP Requests "); // I've restructured this part. I was initially using vis DataSets // for dynamic binding which was easier, but due to performance reasons // I am waiting for the full data to load before I draw var promises = []; - while ((pages > 0) && (iterCount >0)) { + while ((pages > 0) && (iterCount > 0)) { var promise = ZMDataModel.getEvents(0, pages, "none", fromDate, toDate); promises.push(promise); pages--; @@ -304,13 +496,12 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla .then(function (data) { - // create groups - for (var g=0; g<$scope.monitors.length; g++) - { - groups.add({ - id: $scope.monitors[g].Monitor.Id, - content: ZMDataModel.getMonitorName($scope.monitors[g].Monitor.Id) - }); + // create groups + for (var g = 0; g < $scope.monitors.length; g++) { + groups.add({ + id: $scope.monitors[g].Monitor.Id, + content: ZMDataModel.getMonitorName($scope.monitors[g].Monitor.Id) + }); } for (var j = 0; j < data.length; j++) { var myevents = data[j]; @@ -320,13 +511,16 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla graphData.add({ id: graphIndex, - content: '', + content: ""+ myevents[i].Event.Notes + "", start: myevents[i].Event.StartTime, end: myevents[i].Event.EndTime, group: myevents[i].Event.MonitorId, //type: "range", style: "background-color:" + colors[parseInt(myevents[i].Event.MonitorId) % colors.length] + ";border-color:" + colors[parseInt(myevents[i].Event.MonitorId) % colors.length], - // title: "Hello" + myframes: myevents[i].Event.Frames, + mydur: myevents[i].Event.Length, + myeid: myevents[i].Event.Id, + myename: myevents[i].Event.Name, }); @@ -339,21 +533,32 @@ angular.module('zmApp.controllers').controller('zmApp.TimelineCtrl', ['$ionicPla timeline.setItems(graphData); timeline.setGroups(groups); timeline.fit(); - $ionicLoading.hide(); + $ionicLoading.hide(); + timeline.on('select', function (properties) { + if (properties.items && !isNaN(properties.items[0])) { + console.log("You clicked on item " + properties.items); + var item = graphData.get(properties.items); + console.log("ITEM = " + JSON.stringify(item)); + showEvent(item[0].start, item[0].group, item[0].mydur, item[0].myframes, item[0].myeid, item[0].myename); + + + } else { + $ionicLoading.show({ + template: "Zoom in more to scrub events...", + animation: 'fade-in', + showBackdrop: true, + maxWidth: 200, + showDelay: 0, + duration: 1500, + }); + console.log("Zoomed out too far to playback events"); + } + + }); }); // get Events }); } - function arrayObjectIndexOf(myArray, searchTerm, property) { - for (var i = 0, len = myArray.length; i < len; i++) { - if (myArray[i][property] === searchTerm) return i; - } - return -1; - } - - - - }]); -- cgit v1.2.3