[ONOS-3851] Implement default Web GUI page for CPMan

- Reduce the datapoints to 20, resolve cold start problem
- Code refactoring for CpmanViewMessageHandler
- Code refactoring for cpman.js
- Show "No Data" message when client does not receive any data
- Clean up cpman.css
- Specify default colors for charting
- Resolve ArrayIndexOutofBoundsException when the number returned
  dataset is less the number what we expected

Change-Id: I67ab3160ab66f92eaffeffc2d61c7d0e17be0512
diff --git a/apps/cpman/app/src/main/java/org/onosproject/cpman/gui/CpmanViewMessageHandler.java b/apps/cpman/app/src/main/java/org/onosproject/cpman/gui/CpmanViewMessageHandler.java
index 2d4e28f..8003933 100644
--- a/apps/cpman/app/src/main/java/org/onosproject/cpman/gui/CpmanViewMessageHandler.java
+++ b/apps/cpman/app/src/main/java/org/onosproject/cpman/gui/CpmanViewMessageHandler.java
@@ -27,7 +27,6 @@
 import org.onosproject.cpman.ControlMetricType;
 import org.onosproject.cpman.ControlPlaneMonitorService;
 import org.onosproject.net.DeviceId;
-import org.onosproject.net.device.DeviceService;
 import org.onosproject.ui.RequestHandler;
 import org.onosproject.ui.UiMessageHandler;
 import org.onosproject.ui.chart.ChartModel;
@@ -38,14 +37,16 @@
 import java.util.Collection;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.LongStream;
 
 import static org.onosproject.cpman.ControlResource.CONTROL_MESSAGE_METRICS;
 import static org.onosproject.cpman.ControlResource.Type.CONTROL_MESSAGE;
 
 /**
- * CpmanViewMessageHandler class implementation.
+ * Message handler for control plane monitoring view related messages.
  */
 public class CpmanViewMessageHandler extends UiMessageHandler {
 
@@ -55,12 +56,14 @@
     private static final String CPMAN_DATA_RESP = "cpmanDataResponse";
     private static final String CPMANS = "cpmans";
 
-    // TODO: we assume that server side always returns 60 data points
+    // TODO: we assume that server side always returns 20 data points
     // to feed 1 hour time slots, later this should make to be configurable
-    private static final int NUM_OF_DATA_POINTS = 60;
+    private static final int NUM_OF_DATA_POINTS = 20;
 
     private static final int MILLI_CONV_UNIT = 1000;
 
+    private long timestamp = 0L;
+
     @Override
     protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(
@@ -83,50 +86,104 @@
         @Override
         protected void populateChart(ChartModel cm, ObjectNode payload) {
             String uri = string(payload, "devId");
+            ControlPlaneMonitorService cpms = get(ControlPlaneMonitorService.class);
+            ClusterService cs = get(ClusterService.class);
             if (!Strings.isNullOrEmpty(uri)) {
-                Map<ControlMetricType, Long[]> data = Maps.newHashMap();
                 DeviceId deviceId = DeviceId.deviceId(uri);
-                ClusterService cs = get(ClusterService.class);
-                ControlPlaneMonitorService cpms = get(ControlPlaneMonitorService.class);
-
                 if (cpms.availableResources(CONTROL_MESSAGE).contains(deviceId.toString())) {
-                    LocalDateTime ldt = null;
+                    Map<ControlMetricType, Long[]> data = generateMatrix(cpms, cs, deviceId);
+                    LocalDateTime ldt = new LocalDateTime(timestamp * MILLI_CONV_UNIT);
 
-                    try {
-                        for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
-                            ControlLoadSnapshot cls = cpms.getLoad(cs.getLocalNode().id(),
-                                    cmt, NUM_OF_DATA_POINTS, TimeUnit.MINUTES,
-                                    Optional.of(deviceId)).get();
-                            data.put(cmt, ArrayUtils.toObject(cls.recent()));
-                            if (ldt == null) {
-                                ldt = new LocalDateTime(cls.time() * MILLI_CONV_UNIT);
-                            }
-                        }
-
-                        for (int i = 0; i < NUM_OF_DATA_POINTS; i++) {
-                            Map<String, Long> local = Maps.newHashMap();
-                            for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
-                                local.put(StringUtils.lowerCase(cmt.name()), data.get(cmt)[i]);
-                            }
-
-                            local.put(LABEL, ldt.minusMinutes(NUM_OF_DATA_POINTS - i).toDateTime().getMillis());
-
-                            populateMetric(cm.addDataPoint(ldt.minusMinutes(NUM_OF_DATA_POINTS - i)
-                                    .toDateTime().getMillis()), local);
-                        }
-
-                    } catch (InterruptedException | ExecutionException e) {
-                        log.warn(e.getMessage());
-                    }
+                    populateMetrics(cm, data, ldt, NUM_OF_DATA_POINTS);
                 }
             } else {
-                DeviceService ds = get(DeviceService.class);
-                ds.getAvailableDevices();
+                Set<String> deviceIds = cpms.availableResources(CONTROL_MESSAGE);
+                for (String deviceId : deviceIds) {
+                    Map<ControlMetricType, Long> data =
+                            populateDeviceMetrics(cpms, cs, DeviceId.deviceId(deviceId));
+                    Map<String, Long> local = Maps.newHashMap();
+                    for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
+                        local.put(StringUtils.lowerCase(cmt.name()), data.get(cmt));
+                    }
+                    // TODO: need to find a way to present device id using long type
+                    String shortId = StringUtils.substring(deviceId,
+                            deviceId.length() - 2, deviceId.length());
+                    local.put(LABEL, Long.valueOf(shortId));
+                    populateMetric(cm.addDataPoint(Long.valueOf(shortId)), local);
+                }
             }
         }
 
-        private void populateAllDevs(ChartModel.DataPoint dataPoint, Map<String, Long> data) {
+        private Map<ControlMetricType, Long> populateDeviceMetrics(ControlPlaneMonitorService cpms,
+                                                                   ClusterService cs, DeviceId deviceId) {
+            Map<ControlMetricType, Long> data = Maps.newHashMap();
+            for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
+                ControlLoadSnapshot cls;
+                try {
+                    cls = cpms.getLoad(cs.getLocalNode().id(),
+                            cmt, NUM_OF_DATA_POINTS, TimeUnit.MINUTES,
+                            Optional.of(deviceId)).get();
+                    data.put(cmt, Math.round(LongStream.of(cls.recent()).average().getAsDouble()));
+                    timestamp = cls.time();
+                } catch (InterruptedException | ExecutionException e) {
+                    log.warn(e.getMessage());
+                }
+            }
+            return data;
+        }
 
+        private Map<ControlMetricType, Long[]> generateMatrix(ControlPlaneMonitorService cpms,
+                                                              ClusterService cs, DeviceId deviceId) {
+            Map<ControlMetricType, Long[]> data = Maps.newHashMap();
+            for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
+                ControlLoadSnapshot cls;
+                try {
+                    cls = cpms.getLoad(cs.getLocalNode().id(),
+                            cmt, NUM_OF_DATA_POINTS, TimeUnit.MINUTES,
+                            Optional.of(deviceId)).get();
+
+                    // TODO: in some cases, the number of returned dataset is
+                    // less than what we expected (expected -1)
+                    // As a workaround, we simply fill the slot with 0 values,
+                    // such a bug should be fixed with updated RRD4J lib...
+                    data.put(cmt, ArrayUtils.toObject(fillData(cls.recent(), NUM_OF_DATA_POINTS)));
+                    timestamp = cls.time();
+                } catch (InterruptedException | ExecutionException e) {
+                    log.warn(e.getMessage());
+                }
+            }
+            return data;
+        }
+
+        private long[] fillData(long[] origin, int expected) {
+            if (origin.length == expected) {
+                return origin;
+            } else {
+                long[] filled = new long[expected];
+                for (int i = 0; i < expected; i++) {
+                    if (i == 0) {
+                        filled[i] = 0;
+                    } else {
+                        filled[i] = origin[i - 1];
+                    }
+                }
+                return filled;
+            }
+        }
+
+        private void populateMetrics(ChartModel cm, Map<ControlMetricType,
+                Long[]> data, LocalDateTime time, int numOfDp) {
+            for (int i = 0; i < numOfDp; i++) {
+                Map<String, Long> local = Maps.newHashMap();
+                for (ControlMetricType cmt : CONTROL_MESSAGE_METRICS) {
+                    local.put(StringUtils.lowerCase(cmt.name()), data.get(cmt)[i]);
+                }
+
+                local.put(LABEL, time.minusMinutes(numOfDp - i).toDateTime().getMillis());
+
+                populateMetric(cm.addDataPoint(time.minusMinutes(numOfDp - i)
+                        .toDateTime().getMillis()), local);
+            }
         }
 
         private void populateMetric(ChartModel.DataPoint dataPoint,
diff --git a/apps/cpman/app/src/main/resources/app/view/cpman/cpman.css b/apps/cpman/app/src/main/resources/app/view/cpman/cpman.css
index 7cdcc7e..ed68c40 100644
--- a/apps/cpman/app/src/main/resources/app/view/cpman/cpman.css
+++ b/apps/cpman/app/src/main/resources/app/view/cpman/cpman.css
@@ -20,6 +20,7 @@
 
 #ov-cpman {
     padding: 20px;
+    position: relative;
 }
 .light #ov-cpman {
     color: navy;
@@ -40,22 +41,17 @@
     background-color: #444;
 }
 
-#ov-cpman .my-button {
-    cursor: pointer;
-    padding: 4px;
+#ov-cpman #chart-loader {
+    position: absolute;
+    width: 200px;
+    height: 50px;
+    margin-left: -100px;
+    margin-top: -25px;
+    z-index: 900;
+    top: 50%;
     text-align: center;
-}
-
-.light #ov-cpman .my-button {
-    color: white;
-    background-color: #99d;
-}
-.dark #ov-cpman .my-button {
-    color: black;
-    background-color: #aaa;
-}
-
-#ov-cpman .number {
-    font-size: 140%;
-    text-align: right;
+    left: 50%;
+    font-size: 25px;
+    font-weight: bold;
+    color: #ccc;
 }
\ No newline at end of file
diff --git a/apps/cpman/app/src/main/resources/app/view/cpman/cpman.html b/apps/cpman/app/src/main/resources/app/view/cpman/cpman.html
index a07e544..edf24ad 100644
--- a/apps/cpman/app/src/main/resources/app/view/cpman/cpman.html
+++ b/apps/cpman/app/src/main/resources/app/view/cpman/cpman.html
@@ -1,8 +1,21 @@
 <!-- partial HTML -->
 <div id="ov-cpman">
-    <div>
+    <div id="chart-loader" ng-show="!devId && showLoader">
+        No Data
+    </div>
+    <div ng-show="!devId">
+        <canvas id="bar" class="chart chart-bar" chart-data="data"
+                chart-labels="labels" chart-legend="true" chart-click="onClick"
+                chart-series="series" chart-options="options" height="100%">
+        </canvas>
+    </div>
+    <div ng-show="devId">
+        <h2>
+            Chart for Device {{devId || "(No device selected)"}}
+        </h2>
         <canvas id="line" class="chart chart-line" chart-data="data"
-                chart-labels="labels" chart-legend="true" chart-series="series">
+                chart-labels="labels" chart-legend="true"
+                chart-series="series" chart-options="options" height="100%">
         </canvas>
     </div>
 </div>
diff --git a/apps/cpman/app/src/main/resources/app/view/cpman/cpman.js b/apps/cpman/app/src/main/resources/app/view/cpman/cpman.js
index 8474629..30e020a 100644
--- a/apps/cpman/app/src/main/resources/app/view/cpman/cpman.js
+++ b/apps/cpman/app/src/main/resources/app/view/cpman/cpman.js
@@ -21,27 +21,48 @@
     'use strict';
 
     // injected references
-    var $log, $scope, $location, ks, fs, cbs;
+    var $log, $scope, $location, ks, fs, cbs, ns;
 
-    var labels = new Array(60);
-    var data = new Array(new Array(60), new Array(60), new Array(60),
-                         new Array(60), new Array(60), new Array(60));
+    var hasDeviceId;
+
+    var labels = new Array(1);
+    var data = new Array(6);
+    for (var i = 0; i < 6; i++) {
+        data[i] = new Array(1);
+    }
+
+    var date, max, merged;
+
+    function ceil(num) {
+        if (isNaN(num)) {
+            return 0;
+        }
+        var pre = num.toString().length - 1
+        var pow = Math.pow(10, pre);
+        return (Math.ceil(num / pow)) * pow;
+    }
 
     angular.module('ovCpman', ["chart.js"])
         .controller('OvCpmanCtrl',
-        ['$log', '$scope', '$location', 'FnService', 'ChartBuilderService',
+        ['$log', '$scope', '$location', 'FnService', 'ChartBuilderService', 'NavService',
 
-        function (_$log_, _$scope_, _$location_, _fs_, _cbs_) {
+        function (_$log_, _$scope_, _$location_, _fs_, _cbs_, _ns_) {
             var params;
             $log = _$log_;
             $scope = _$scope_;
             $location = _$location_;
             fs = _fs_;
             cbs = _cbs_;
+            ns = _ns_;
 
             params = $location.search();
+
             if (params.hasOwnProperty('devId')) {
                 $scope.devId = params['devId'];
+                hasDeviceId = true;
+            } else {
+                $scope.type = 'StackedBar';
+                hasDeviceId = false;
             }
 
             cbs.buildChart({
@@ -50,31 +71,70 @@
                 query: params
             });
 
-            var idx = 0;
-            var date;
             $scope.$watch('chartData', function () {
-                idx = 0;
                 if (!fs.isEmptyObject($scope.chartData)) {
-                    $scope.chartData.forEach(function (cm) {
+                    $scope.showLoader = false;
+                    var length = $scope.chartData.length;
+                    labels = new Array(length);
+                    for (var i = 0; i < 6; i++) {
+                        data[i] = new Array(length);
+                    }
+
+                    $scope.chartData.forEach(function (cm, idx) {
                         data[0][idx] = cm.inbound_packet;
                         data[1][idx] = cm.outbound_packet;
                         data[2][idx] = cm.flow_mod_packet;
                         data[3][idx] = cm.flow_removed_packet;
                         data[4][idx] = cm.request_packet;
                         data[5][idx] = cm.reply_packet;
-                        date = new Date(cm.label);
-                        labels[idx] = date.getHours() + ":" + date.getMinutes();
-                        idx++;
+
+                        if(hasDeviceId) {
+                            date = new Date(cm.label);
+                            labels[idx] = date.getHours() + ":" + date.getMinutes();
+                        } else {
+                            labels[idx] = cm.label;
+                        }
                     });
                 }
+
+                merged = [].concat.apply([], data);
+                max = Math.max.apply(null, merged);
+                $scope.labels = labels;
+                $scope.data = data;
+                $scope.options = {
+                    scaleOverride : true,
+                    scaleSteps : 10,
+                    scaleStepWidth : ceil(max) / 10,
+                    scaleStartValue : 0
+                };
+                $scope.onClick = function (points, evt) {
+                    if (points[0]) {
+                        // TODO: this will be replaced with real device id
+                        var tmpId = 'of:000000000000020' + points[0].label;
+                        ns.navTo('cpman', { devId: tmpId });
+                        $log.log(points[0].label);
+                    }
+                };
             });
 
             $scope.series = ['INBOUND', 'OUTBOUND', 'FLOW-MOD',
                              'FLOW-REMOVED', 'STATS-REQUEST', 'STATS-REPLY'];
             $scope.labels = labels;
-
             $scope.data = data;
 
+            $scope.chartColors = [
+                      '#286090',
+                      '#F7464A',
+                      '#46BFBD',
+                      '#FDB45C',
+                      '#97BBCD',
+                      '#4D5360',
+                      '#8c4f9f'
+                    ];
+            Chart.defaults.global.colours = $scope.chartColors;
+
+            $scope.showLoader = true;
+
             $log.log('OvCpmanCtrl has been created');
         }]);