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" : "");
+        }
+    }
 }
diff --git a/core/api/src/test/java/org/onosproject/ui/topo/TopoUtilsTest.java b/core/api/src/test/java/org/onosproject/ui/topo/TopoUtilsTest.java
new file mode 100644
index 0000000..f7b56eb
--- /dev/null
+++ b/core/api/src/test/java/org/onosproject/ui/topo/TopoUtilsTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.ui.topo;
+
+import org.junit.Test;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultLink;
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+import org.onosproject.net.provider.ProviderId;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.onosproject.net.ConnectPoint.deviceConnectPoint;
+
+/**
+ * Unit tests for {@link TopoUtils}.
+ */
+public class TopoUtilsTest {
+    private static final String AM_WL = "wrong label";
+    private static final String AM_WM = "wrong magnitude";
+    private static final String AM_CL = "clipped?";
+    private static final String AM_NCL = "not clipped?";
+
+    private static final ConnectPoint CP_FU = deviceConnectPoint("fu:001/3");
+    private static final ConnectPoint CP_BAH = deviceConnectPoint("bah:002/5");
+
+    private static final Link LINK_FU_BAH = DefaultLink.builder()
+            .src(CP_FU)
+            .dst(CP_BAH)
+            .type(Link.Type.DIRECT)
+            .providerId(ProviderId.NONE)
+            .build();
+
+    private static final Link LINK_BAH_FU = DefaultLink.builder()
+            .src(CP_BAH)
+            .dst(CP_FU)
+            .type(Link.Type.DIRECT)
+            .providerId(ProviderId.NONE)
+            .build();
+
+    private TopoUtils.ValueLabel vl;
+
+    @Test
+    public void linkStringFuBah() {
+        String compact = TopoUtils.compactLinkString(LINK_FU_BAH);
+        assertEquals("wrong link id", "fu:001/3-bah:002/5", compact);
+    }
+
+    @Test
+    public void linkStringBahFu() {
+        String compact = TopoUtils.compactLinkString(LINK_BAH_FU);
+        assertEquals("wrong link id", "bah:002/5-fu:001/3", compact);
+    }
+
+    @Test
+    public void canonLinkKey() {
+        LinkKey fb = TopoUtils.canonicalLinkKey(LINK_FU_BAH);
+        LinkKey bf = TopoUtils.canonicalLinkKey(LINK_BAH_FU);
+        assertEquals("not canonical", fb, bf);
+    }
+
+    @Test
+    public void formatSmallBytes() {
+        vl = TopoUtils.formatBytes(1_000L);
+        assertEquals(AM_WM, TopoUtils.Magnitude.ONE, vl.magnitude());
+        assertEquals(AM_WL, "1,000 B", vl.toString());
+    }
+
+    @Test
+    public void formatKiloBytes() {
+        vl = TopoUtils.formatBytes(2_000L);
+        assertEquals(AM_WM, TopoUtils.Magnitude.KILO, vl.magnitude());
+        assertEquals(AM_WL, "1.95 KB", vl.toString());
+    }
+
+    @Test
+    public void formatMegaBytes() {
+        vl = TopoUtils.formatBytes(3_000_000L);
+        assertEquals(AM_WM, TopoUtils.Magnitude.MEGA, vl.magnitude());
+        assertEquals(AM_WL, "2.86 MB", vl.toString());
+    }
+
+    @Test
+    public void formatGigaBytes() {
+        vl = TopoUtils.formatBytes(4_000_000_000L);
+        assertEquals(AM_WM, TopoUtils.Magnitude.GIGA, vl.magnitude());
+        assertEquals(AM_WL, "3.73 GB", vl.toString());
+    }
+
+    @Test
+    public void formatTeraBytes() {
+        vl = TopoUtils.formatBytes(5_000_000_000_000L);
+        assertEquals(AM_WM, TopoUtils.Magnitude.GIGA, vl.magnitude());
+        assertEquals(AM_WL, "4,656.61 GB", vl.toString());
+    }
+
+    @Test
+    public void formatPacketRateSmall() {
+        vl = TopoUtils.formatPacketRate(37);
+        assertEquals(AM_WL, "37 pps", vl.toString());
+    }
+
+    @Test
+    public void formatPacketRateKilo() {
+        vl = TopoUtils.formatPacketRate(1024);
+        assertEquals(AM_WL, "1 Kpps", vl.toString());
+    }
+
+    @Test
+    public void formatPacketRateKilo2() {
+        vl = TopoUtils.formatPacketRate(1034);
+        assertEquals(AM_WL, "1.01 Kpps", vl.toString());
+    }
+
+    @Test
+    public void formatPacketRateMega() {
+        vl = TopoUtils.formatPacketRate(9_000_000);
+        assertEquals(AM_WL, "8.58 Mpps", vl.toString());
+    }
+
+    // remember for the following method calls, the input is in bytes!
+    @Test
+    public void formatClippedBitsSmall() {
+        vl = TopoUtils.formatClippedBitRate(8);
+        assertEquals(AM_WL, "64 bps", vl.toString());
+        assertFalse(AM_CL, vl.clipped());
+    }
+
+    @Test
+    public void formatClippedBitsKilo() {
+        vl = TopoUtils.formatClippedBitRate(2_004);
+        assertEquals(AM_WL, "15.66 Kbps", vl.toString());
+        assertFalse(AM_CL, vl.clipped());
+    }
+
+    @Test
+    public void formatClippedBitsMega() {
+        vl = TopoUtils.formatClippedBitRate(3_123_123);
+        assertEquals(AM_WL, "23.83 Mbps", vl.toString());
+        assertFalse(AM_CL, vl.clipped());
+    }
+
+    @Test
+    public void formatClippedBitsGiga() {
+        vl = TopoUtils.formatClippedBitRate(500_000_000);
+        assertEquals(AM_WL, "3.73 Gbps", vl.toString());
+        assertFalse(AM_CL, vl.clipped());
+    }
+
+    @Test
+    public void formatClippedBitsGigaExceedThreshold() {
+        vl = TopoUtils.formatClippedBitRate(5_000_000_000L);
+        // approx. 37.25 Gbps
+        assertEquals(AM_WL, "10 Gbps", vl.toString());
+        assertTrue(AM_NCL, vl.clipped());
+    }
+
+    @Test
+    public void formatNoFlows() {
+        String f = TopoUtils.formatFlows(0);
+        assertEquals(AM_WL, "", f);
+    }
+
+    @Test
+    public void formatNegativeFlows() {
+        String f = TopoUtils.formatFlows(-3);
+        assertEquals(AM_WL, "", f);
+    }
+
+    @Test
+    public void formatOneFlow() {
+        String f = TopoUtils.formatFlows(1);
+        assertEquals(AM_WL, "1 flow", f);
+    }
+
+    @Test
+    public void formatManyFlows() {
+        String f = TopoUtils.formatFlows(42);
+        assertEquals(AM_WL, "42 flows", f);
+    }
+}