Refactored Traffic Monitor code to display packets / second.
- cleaned up "rate thresholds" for coloring links.
- added unit tests for TopoUtils.
- "Monitor All Traffic" button on toolbar now cycles between 3 modes.

Change-Id: If33cfb3e6d6190e1321752b6d058274d3004f309
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/TopoUtils.java b/core/api/src/main/java/org/onosproject/ui/topo/TopoUtils.java
index 3ed50f8..2ab2512 100644
--- a/core/api/src/main/java/org/onosproject/ui/topo/TopoUtils.java
+++ b/core/api/src/main/java/org/onosproject/ui/topo/TopoUtils.java
@@ -21,6 +21,7 @@
 
 import java.text.DecimalFormat;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static org.onosproject.net.LinkKey.linkKey;
 
 /**
@@ -29,27 +30,20 @@
  */
 public final class TopoUtils {
 
-    // explicit decision made to not 'javadoc' these self explanatory constants
-    public static final double KILO = 1024;
-    public static final double MEGA = 1024 * KILO;
-    public static final double GIGA = 1024 * MEGA;
+    // explicit decision made to not 'javadoc' these constants
+    public static final double N_KILO = 1024;
+    public static final double N_MEGA = 1024 * N_KILO;
+    public static final double N_GIGA = 1024 * N_MEGA;
 
-    public static final String GBITS_UNIT = "Gb";
-    public static final String MBITS_UNIT = "Mb";
-    public static final String KBITS_UNIT = "Kb";
     public static final String BITS_UNIT = "b";
-    public static final String GBYTES_UNIT = "GB";
-    public static final String MBYTES_UNIT = "MB";
-    public static final String KBYTES_UNIT = "KB";
     public static final String BYTES_UNIT = "B";
-
+    public static final String PACKETS_UNIT = "p";
 
     private static final DecimalFormat DF2 = new DecimalFormat("#,###.##");
 
     private static final String COMPACT = "%s/%s-%s/%s";
     private static final String EMPTY = "";
     private static final String SPACE = " ";
-    private static final String PER_SEC = "ps";
     private static final String FLOW = "flow";
     private static final String FLOWS = "flows";
 
@@ -65,7 +59,7 @@
      */
     public static String compactLinkString(Link link) {
         return String.format(COMPACT, link.src().elementId(), link.src().port(),
-                             link.dst().elementId(), link.dst().port());
+                link.dst().elementId(), link.dst().port());
     }
 
     /**
@@ -83,64 +77,36 @@
     }
 
     /**
-     * Returns human readable count of bytes, to be displayed as a label.
+     * Returns a value representing a count of bytes.
      *
      * @param bytes number of bytes
-     * @return formatted byte count
+     * @return value representing bytes
      */
-    public static String formatBytes(long bytes) {
-        String unit;
-        double value;
-        if (bytes > GIGA) {
-            value = bytes / GIGA;
-            unit = GBYTES_UNIT;
-        } else if (bytes > MEGA) {
-            value = bytes / MEGA;
-            unit = MBYTES_UNIT;
-        } else if (bytes > KILO) {
-            value = bytes / KILO;
-            unit = KBYTES_UNIT;
-        } else {
-            value = bytes;
-            unit = BYTES_UNIT;
-        }
-        return DF2.format(value) + SPACE + unit;
+    public static ValueLabel formatBytes(long bytes) {
+        return new ValueLabel(bytes, BYTES_UNIT);
     }
 
     /**
-     * Returns human readable bit rate, to be displayed as a label.
+     * Returns a value representing a count of packets per second.
+     *
+     * @param packets number of packets (per second)
+     * @return value representing packets per second
+     */
+    public static ValueLabel formatPacketRate(long packets) {
+        return new ValueLabel(packets, PACKETS_UNIT).perSec();
+    }
+
+
+    /**
+     * Returns a value representing a count of bits per second,
+     * (clipped to a maximum of 10 Gbps).
+     * Note that the input is bytes per second.
      *
      * @param bytes bytes per second
-     * @return formatted bits per second
+     * @return value representing bits per second
      */
-    public static String formatBitRate(long bytes) {
-        String unit;
-        double value;
-
-        //Convert to bits
-        long bits = bytes * 8;
-        if (bits > GIGA) {
-            value = bits / GIGA;
-            unit = GBITS_UNIT;
-
-            // NOTE: temporary hack to clip rate at 10.0 Gbps
-            //  Added for the CORD Fabric demo at ONS 2015
-            // TODO: provide a more elegant solution to this issue
-            if (value > 10.0) {
-                value = 10.0;
-            }
-
-        } else if (bits > MEGA) {
-            value = bits / MEGA;
-            unit = MBITS_UNIT;
-        } else if (bits > KILO) {
-            value = bits / KILO;
-            unit = KBITS_UNIT;
-        } else {
-            value = bits;
-            unit = BITS_UNIT;
-        }
-        return DF2.format(value) + SPACE + unit + PER_SEC;
+    public static ValueLabel formatClippedBitRate(long bytes) {
+        return new ValueLabel(bytes * 8, BITS_UNIT).perSec().clipG(10.0);
     }
 
     /**
@@ -155,4 +121,150 @@
         }
         return String.valueOf(flows) + SPACE + (flows > 1 ? FLOWS : FLOW);
     }
+
+
+    /**
+     * Enumeration of magnitudes.
+     */
+    public enum Magnitude {
+        ONE("", 1),
+        KILO("K", N_KILO),
+        MEGA("M", N_MEGA),
+        GIGA("G", N_GIGA);
+
+        private final String label;
+        private final double mult;
+
+        Magnitude(String label, double mult) {
+            this.label = label;
+            this.mult = mult;
+        }
+
+        @Override
+        public String toString() {
+            return label;
+        }
+
+        private double mult() {
+            return mult;
+        }
+    }
+
+
+    /**
+     * Encapsulates a value to be used as a label.
+     */
+    public static class ValueLabel {
+        private final long value;
+        private final String unit;
+
+        private double divDown;
+        private Magnitude mag;
+
+        private boolean perSec = false;
+        private boolean clipped = false;
+
+        /**
+         * Creates a value label with the given base value and unit. For
+         * example:
+         * <pre>
+         * ValueLabel bits = new ValueLabel(2_050, "b");
+         * ValueLabel bytesPs = new ValueLabel(3_000_000, "B").perSec();
+         * </pre>
+         * Generating labels:
+         * <pre>
+         *   bits.toString()     ...  "2.00 Kb"
+         *   bytesPs.toString()  ...  "2.86 MBps"
+         * </pre>
+         *
+         * @param value the base value
+         * @param unit  the value unit
+         */
+        public ValueLabel(long value, String unit) {
+            this.value = value;
+            this.unit = unit;
+            computeAdjusted();
+        }
+
+        private void computeAdjusted() {
+            if (value >= N_GIGA) {
+                divDown = value / N_GIGA;
+                mag = Magnitude.GIGA;
+            } else if (value >= N_MEGA) {
+                divDown = value / N_MEGA;
+                mag = Magnitude.MEGA;
+            } else if (value >= N_KILO) {
+                divDown = value / N_KILO;
+                mag = Magnitude.KILO;
+            } else {
+                divDown = value;
+                mag = Magnitude.ONE;
+            }
+        }
+
+        /**
+         * Mark this value to be expressed as a rate. That is, "ps" (per sec)
+         * will be appended in the string representation.
+         *
+         * @return self, for chaining
+         */
+        public ValueLabel perSec() {
+            perSec = true;
+            return this;
+        }
+
+        /**
+         * Clips the (adjusted) value to the given threshold expressed in
+         * Giga units. That is, if the adjusted value exceeds the threshold,
+         * it will be set to the threshold value and the clipped flag
+         * will be set. For example,
+         * <pre>
+         * ValueLabel tooMuch = new ValueLabel(12_000_000_000, "b")
+         *      .perSec().clipG(10.0);
+         *
+         * tooMuch.toString()    ...  "10.00 Gbps"
+         * tooMuch.clipped()     ...  true
+         * </pre>
+         *
+         * @param threshold the clip threshold (Giga)
+         * @return self, for chaining
+         */
+        public ValueLabel clipG(double threshold) {
+            return clip(threshold, Magnitude.GIGA);
+        }
+
+        private ValueLabel clip(double threshold, Magnitude m) {
+            checkArgument(threshold >= 1.0, "threshold must be 1.0 or more");
+            double clipAt = threshold * m.mult();
+            if (value > clipAt) {
+                divDown = threshold;
+                mag = m;
+                clipped = true;
+            }
+            return this;
+        }
+
+        /**
+         * Returns true if this value was clipped to a maximum threshold.
+         *
+         * @return true if value was clipped
+         */
+        public boolean clipped() {
+            return clipped;
+        }
+
+        /**
+         * Returns the magnitude value.
+         *
+         * @return the magnitude
+         */
+        public Magnitude magnitude() {
+            return mag;
+        }
+
+        @Override
+        public String toString() {
+            return DF2.format(divDown) + SPACE + mag + unit + (perSec ? "ps" : "");
+        }
+    }
 }