GUI -- TopoView - re-implemented All/Pkt/Opt filter radio buttons.

Change-Id: I41cf0eca60a685606a631c0edf4779d7730bb649
diff --git a/web/gui/src/main/webapp/app/fw/mast/mast.css b/web/gui/src/main/webapp/app/fw/mast/mast.css
index 38baba1..3c7d97d 100644
--- a/web/gui/src/main/webapp/app/fw/mast/mast.css
+++ b/web/gui/src/main/webapp/app/fw/mast/mast.css
@@ -57,3 +57,11 @@
 .dark #mast .title {
     color: #eee;
 }
+
+#mast-right {
+    display: inline-block;
+    padding-top: 8px;
+    padding-right: 16px;
+    float: right;
+    /*border: 1px solid red;*/
+}
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/app/fw/mast/mast.html b/web/gui/src/main/webapp/app/fw/mast/mast.html
index e10d6ca..59c99a3 100644
--- a/web/gui/src/main/webapp/app/fw/mast/mast.html
+++ b/web/gui/src/main/webapp/app/fw/mast/mast.html
@@ -1,3 +1,4 @@
 <!-- Masthead partial HTML -->
 <img class="logo" src="../data/img/onos-logo.png" ng-click="mastCtrl.toggleNav()">
 <span class="title">Open Network Operating System</span>
+<div id="mast-right"></div>
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.css b/web/gui/src/main/webapp/app/view/topo/topo.css
index b9613d4..f725634 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -515,3 +515,40 @@
 .dark #ov-topo svg .linkLabel text {
     stroke: #777;
 }
+
+
+/* radio buttons injected into masthead */
+
+#topo-radio-group span.radio {
+    font-size: 10pt;
+    margin: 4px 2px;
+    padding: 1px 6px;
+    -moz-border-radius: 3px;
+    border-radius: 3px;
+    cursor: pointer;
+}
+
+.light #topo-radio-group span.radio {
+    border: 1px dotted #222;
+    color: #eee;
+}
+.dark #topo-radio-group span.radio {
+    border: 1px dotted #bbb;
+    color: #888;
+}
+
+#topo-radio-group span.radio.active {
+    padding: 1px 6px;
+}
+
+.light #topo-radio-group span.radio.active {
+    background-color: #bbb;
+    border: 1px solid #eee;
+    color: #666;
+
+}
+.dark #topo-radio-group span.radio.active {
+    background-color: #222;
+    border: 1px solid #eee;
+    color: #78a;
+}
diff --git a/web/gui/src/main/webapp/app/view/topo/topoFilter.js b/web/gui/src/main/webapp/app/view/topo/topoFilter.js
index eba31ae..c2e7a62 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoFilter.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoFilter.js
@@ -29,11 +29,129 @@
     // api to topoForce
     var api;
     /*
-     node()                         // get ref to D3 selection of nodes
+     node()     // get ref to D3 selection of nodes
+     link()     // get ref to D3 selection of links
      */
 
-    // internal state
+    // which "layer" a particular item "belongs to"
+    var layerLookup = {
+        host: {
+            endstation: 'pkt', // default, if host event does not define type
+            router:     'pkt',
+            bgpSpeaker: 'pkt'
+        },
+        device: {
+            switch: 'pkt',
+            roadm: 'opt'
+        },
+        link: {
+            hostLink: 'pkt',
+            direct: 'pkt',
+            indirect: '',
+            tunnel: '',
+            optical: 'opt'
+        }
+    };
 
+    var idPrefix = 'topo-rb-';
+
+    var dispatch = {
+            all: function () { suppressLayers(false); },
+            pkt: function () { showLayer('pkt'); },
+            opt: function () { showLayer('opt'); }
+        },
+        filterButtons = [
+            { text: 'All Layers', id: 'all' },
+            { text: 'Packet Only', id: 'pkt' },
+            { text: 'Optical Only', id: 'opt' }
+        ],
+        btnG,
+        btnDef = {},
+        targetDiv;
+
+
+    function injectButtons(div) {
+        targetDiv = div;
+
+        btnG = div.append('div').attr('id', 'topo-radio-group');
+
+        filterButtons.forEach(function (btn, i) {
+            var bid = btn.id,
+                txt = btn.text,
+                uid = idPrefix + bid,
+                button = btnG.append('span')
+                    .attr({
+                        id: uid,
+                        'class': 'radio'
+                    })
+                    .text(txt);
+            btnDef[uid] = btn;
+
+            if (i === 0) {
+                button.classed('active', true);
+                btnG.selected = bid;
+            }
+        });
+
+        btnG.selectAll('span')
+            .on('click', function () {
+               var button = d3.select(this),
+                   uid = button.attr('id'),
+                   btn = btnDef[uid],
+                   act = button.classed('active');
+
+                if (!act) {
+                    btnG.selectAll('span').classed('active', false);
+                    button.classed('active', true);
+                    btnG.selected = btn.id;
+                    clickAction(btn.id);
+                }
+            });
+    }
+
+    function clickAction(which) {
+        dispatch[which]();
+    }
+
+    function selected() {
+        return btnG ? btnG.selected : '';
+    }
+
+    // code to manipulate the nodes and links as per the filter settings
+    function inLayer(d, layer) {
+        var type = d.class === 'link' ? d.type() : d.type,
+            look = layerLookup[d.class],
+            lyr = look && look[type];
+        return lyr === layer;
+    }
+
+    function unsuppressLayer(which) {
+        api.node().each(function (d) {
+            var node = d.el;
+            if (inLayer(d, which)) {
+                node.classed('suppressed', false);
+            }
+        });
+
+        api.link().each(function (d) {
+            var link = d.el;
+            if (inLayer(d, which)) {
+                link.classed('suppressed', false);
+            }
+        });
+    }
+
+    function suppressLayers(b) {
+        api.node().classed('suppressed', b);
+        api.link().classed('suppressed', b);
+//        d3.selectAll('svg .port').classed('inactive', false);
+//        d3.selectAll('svg .portText').classed('inactive', false);
+    }
+
+    function showLayer(which) {
+        suppressLayers(true);
+        unsuppressLayer(which);
+    }
 
     // === -----------------------------------------------------
     // === MODULE DEFINITION ===
@@ -52,15 +170,23 @@
                 tps = _tps_;
                 tts = _tts_;
 
-                function initFilter(_api_) {
+                function initFilter(_api_, div) {
                     api = _api_;
+                    injectButtons(div);
                 }
 
-                function destroyFilter() { }
+                function destroyFilter() {
+                    targetDiv.select('#topo-radio-group').remove();
+                    btnG = null;
+                    btnDef = {};
+                }
 
                 return {
                     initFilter: initFilter,
-                    destroyFilter: destroyFilter
+                    destroyFilter: destroyFilter,
+
+                    clickAction: clickAction,
+                    selected: selected
                 };
             }]);
 }());
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index 10e924f..7dadaeb 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -23,7 +23,8 @@
     'use strict';
 
     // injected refs
-    var $log, fs, sus, is, ts, flash, tis, tms, tss, tts, icfg, uplink;
+    var $log, fs, sus, is, ts, flash, tis, tms, tss, tts, fltr,
+        icfg, uplink;
 
     // configuration
     var labelConfig = {
@@ -1078,14 +1079,21 @@
         }
     }
 
+    function mkFilterApi(uplink) {
+        return {
+            node: function () { return node; },
+            link: function () { return link; }
+        };
+    }
+
     angular.module('ovTopo')
     .factory('TopoForceService',
         ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
             'FlashService', 'TopoInstService', 'TopoModelService',
-            'TopoSelectService', 'TopoTrafficService',
+            'TopoSelectService', 'TopoTrafficService', 'TopoFilterService',
 
         function (_$log_, _fs_, _sus_, _is_, _ts_, _flash_,
-                  _tis_, _tms_, _tss_, _tts_) {
+                  _tis_, _tms_, _tss_, _tts_, _fltr_) {
             $log = _$log_;
             fs = _fs_;
             sus = _sus_;
@@ -1096,6 +1104,7 @@
             tms = _tms_;
             tss = _tss_;
             tts = _tts_;
+            fltr = _fltr_;
 
             icfg = is.iconConfig();
 
@@ -1117,6 +1126,7 @@
                 tms.initModel(mkModelApi(uplink), dim);
                 tss.initSelect(mkSelectApi(uplink));
                 tts.initTraffic(mkTrafficApi(uplink));
+                fltr.initFilter(mkFilterApi(uplink), d3.select('#mast-right'));
 
                 settings = angular.extend({}, defaultSettings, opts);
 
@@ -1151,6 +1161,7 @@
             }
 
             function destroyForce() {
+                fltr.destroyFilter();
                 tts.destroyTraffic();
                 tss.destroySelect();
                 tms.destroyModel();
diff --git a/web/gui/src/main/webapp/tests/app/view/topo/topoFilter-spec.js b/web/gui/src/main/webapp/tests/app/view/topo/topoFilter-spec.js
index 983469c..79a4398 100644
--- a/web/gui/src/main/webapp/tests/app/view/topo/topoFilter-spec.js
+++ b/web/gui/src/main/webapp/tests/app/view/topo/topoFilter-spec.js
@@ -18,25 +18,80 @@
  ONOS GUI -- Topo View -- Topo Filter Service - Unit Tests
  */
 describe('factory: view/topo/topoFilter.js', function() {
-    var $log, fs, filter;
+    var $log, fs, fltr, d3Elem, api;
+
+    var mockNodes = {
+            each: function () {},
+            classed: function () {}
+        },
+        mockLinks = {
+            each: function () {},
+            classed: function () {}
+        };
 
     beforeEach(module('ovTopo', 'onosUtil', 'onosLayer'));
 
     beforeEach(inject(function (_$log_, FnService, TopoFilterService) {
         $log = _$log_;
         fs = FnService;
-        filter = TopoFilterService;
+        fltr = TopoFilterService;
+        d3Elem = d3.select('body').append('div').attr('id', 'myMastDiv');
+
+        api = {
+            node: function () { return mockNodes; },
+            link: function () { return mockLinks; }
+        };
     }));
 
+    afterEach(function () {
+        d3.select('#myMastDiv').remove();
+    });
+
     it('should define TopoFilterService', function () {
-        expect(filter).toBeDefined();
+        expect(fltr).toBeDefined();
     });
 
     it('should define api functions', function () {
-        expect(fs.areFunctions(filter, [
-            'initFilter', 'destroyFilter'
+        expect(fs.areFunctions(fltr, [
+            'initFilter', 'destroyFilter',
+            'clickAction', 'selected'
         ])).toBeTruthy();
     });
 
-    // TODO: more tests...
+    it('should inject the buttons into the given div', function () {
+        fltr.initFilter(api, d3Elem);
+        var grpdiv = d3Elem.select('#topo-radio-group');
+        expect(grpdiv.size()).toBe(1);
+
+        var btns = grpdiv.selectAll('span');
+        expect(btns.size()).toBe(3);
+
+        var prefix = 'topo-rb-',
+            expIds = [ 'all', 'pkt', 'opt' ];
+
+        btns.each(function (d, i) {
+            var b = d3.select(this);
+            expect(b.attr('id')).toEqual(prefix + expIds[i]);
+            // 0th button is active - others are not
+            expect(b.classed('active')).toEqual(i === 0);
+        });
+    });
+
+    it('should remove the buttons from the given div', function () {
+        fltr.initFilter(api, d3Elem);
+        var grpdiv = d3Elem.select('#topo-radio-group');
+        expect(grpdiv.size()).toBe(1);
+
+        fltr.destroyFilter();
+        grpdiv = d3Elem.select('#topo-radio-group');
+        expect(grpdiv.size()).toBe(0);
+    });
+
+    it('should report the selected button', function () {
+        fltr.initFilter(api, d3Elem);
+        expect(fltr.selected()).toEqual('all');
+    });
+
+    // TODO: figure out how to trigger the click function on the spans..
+
 });