Topo2: Reset node position and unpin
Refactored NodeModel
Added class to the surrounding rect for selected class
Renamed Panels to avoid conflict with classic topo
Topo2: Details Panel for single device selection
Topo2: Added Equalize Masters keyboard shortcut
Topo2: Toggle Link Port highlighting
Topo2: Node Labels was returning empty string
       if friendly name was null
Topo2: Reset map zoom and panning

Change-Id: I0a949b2f8205e1abcfcac5aaec65c18d76e77cff
diff --git a/web/gui/src/main/webapp/app/view/topo2/.eslintrc.js b/web/gui/src/main/webapp/app/view/topo2/.eslintrc.js
index 5934858..8432f7a 100644
--- a/web/gui/src/main/webapp/app/view/topo2/.eslintrc.js
+++ b/web/gui/src/main/webapp/app/view/topo2/.eslintrc.js
@@ -3,7 +3,8 @@
     "installedESLint": true,
     "globals": {
         "angular": true,
-        "d3": true
+        "d3": true,
+        "_": true
     },
     "rules": {
         "brace-style": 0,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css b/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
index 7d8be0b..a5b996a 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
@@ -181,7 +181,7 @@
 }
 
 
-#ov-topo2 svg .node.device.selected rect {
+#ov-topo2 svg .node.device.selected .node-container {
     stroke-width: 2.0;
     stroke: #009fdb;
 }
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2.css b/web/gui/src/main/webapp/app/view/topo2/topo2.css
index 2851cbd..cda26af 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.css
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.css
@@ -99,3 +99,29 @@
     cursor: pointer;
     fill-rule: evenodd;
 }
+
+/* --- Topo Summary Panel --- */
+
+#topo2-p-summary {
+    padding: 16px;
+}
+
+
+/* --- Topo Detail Panel --- */
+
+#topo2-p-detail {
+    padding: 16px;
+    top: 370px;
+}
+html[data-platform='iPad'] #topo2-p-detail {
+    top: 386px;
+}
+
+#topo2-p-detail .actionBtns .actionBtn {
+    display: inline-block;
+}
+#topo2-p-detail .actionBtns .actionBtn svg {
+    width: 28px;
+    height: 28px;
+}
+
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2.js b/web/gui/src/main/webapp/app/view/topo2/topo2.js
index 5c8332c..48f06d0 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.js
@@ -24,7 +24,7 @@
     'use strict';
 
     // references to injected services
-    var $scope, $log, fs, mast, ks, zs,
+    var $scope, $log, fs, mast, ks,
         gs, sus, ps, t2es, t2fs, t2is, t2bcs, t2kcs, t2ms, t2mcs, t2zs;
 
     // DOM elements
@@ -81,19 +81,20 @@
     angular.module('ovTopo2', ['onosUtil', 'onosSvg', 'onosRemote'])
     .controller('OvTopo2Ctrl',
         ['$scope', '$log', '$location',
-        'FnService', 'MastService', 'KeyService', 'ZoomService',
+        'FnService', 'MastService', 'KeyService',
         'GlyphService', 'MapService', 'SvgUtilService', 'FlashService',
         'WebSocketService', 'PrefsService', 'ThemeService',
         'Topo2EventService', 'Topo2ForceService', 'Topo2InstanceService',
         'Topo2BreadcrumbService', 'Topo2KeyCommandService', 'Topo2MapService',
-        'Topo2MapConfigService', 'Topo2SummaryPanelService', 'Topo2ZoomService',
+        'Topo2MapConfigService', 'Topo2ZoomService',
+        'Topo2SummaryPanelService', 'Topo2DeviceDetailsPanel',
 
         function (_$scope_, _$log_, _$loc_,
-            _fs_, _mast_, _ks_, _zs_,
+            _fs_, _mast_, _ks_,
             _gs_, _ms_, _sus_, _flash_,
             _wss_, _ps_, _th_,
             _t2es_, _t2fs_, _t2is_, _t2bcs_, _t2kcs_, _t2ms_, _t2mcs_,
-            summaryPanel, _t2zs_
+            _t2zs_, summaryPanel, detailsPanel
         ) {
 
             var params = _$loc_.search(),
@@ -115,7 +116,6 @@
             fs = _fs_;
             mast = _mast_;
             ks = _ks_;
-            zs = _zs_;
 
             gs = _gs_;
             sus = _sus_;
@@ -156,6 +156,8 @@
                 ks.unbindKeys();
                 t2fs.destroy();
                 t2is.destroy();
+                summaryPanel.destroy();
+                detailsPanel.destroy();
             });
 
             // svg layer and initialization of components
@@ -176,7 +178,7 @@
             t2es.bindHandlers();
 
             // Add the map SVG Group
-            t2ms.init(zoomLayer).then(
+            t2ms.init(zoomLayer, zoomer).then(
                 function (proj) {
                     var z = ps.getPrefs('topo_zoom', { tx: 0, ty: 0, sc: 1 });
                     zoomer.panZoom([z.tx, z.ty], z.sc);
@@ -219,6 +221,7 @@
             // $log.debug('registered overlays...', tov.list());
 
             summaryPanel.init();
+            detailsPanel.init();
 
             $log.log('OvTopo2Ctrl has been created');
         }]);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
index 12b0a13..0d0d691 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
@@ -34,10 +34,10 @@
         var DeviceCollection = Collection.extend({
             model: Model,
             comparator: function (a, b) {
-                var order = region.get('layerOrder'),
-                    aLayer = a.get('layer'),
-                    bLayer = b.get('layer');
-                return order.indexOf(aLayer - order.indexOf(bLayer));
+                // var order = region.get('layerOrder'),
+                //     aLayer = a.get('id'),
+                //     bLayer = b.get('layer');
+                // return order.indexOf(aLayer - order.indexOf(bLayer));
             }
         });
 
@@ -54,11 +54,10 @@
         return deviceCollection;
     }
 
-
     angular.module('ovTopo2')
     .factory('Topo2DeviceService',
-        ['Topo2Collection', 'Topo2NodeModel',
-            function (_c_, _nm_) {
+        ['Topo2Collection', 'Topo2NodeModel', 'Topo2DeviceDetailsPanel',
+            function (_c_, _nm_, detailsPanel) {
 
                 Collection = _c_;
 
@@ -67,11 +66,27 @@
                         this.super = this.constructor.__super__;
                         this.super.initialize.apply(this, arguments);
                     },
+                    events: {
+                        'click': 'onClick'
+                    },
                     nodeType: 'device',
                     icon: function () {
                         var type = this.get('type');
                         return remappedDeviceTypes[type] || type || 'unknown';
                     },
+                    onClick: function () {
+
+                        if (this.get('selected')) {
+                            this.set('selected', false);
+                            detailsPanel.hide();
+                        } else {
+                            this.set('selected', true);
+                            detailsPanel.updateDetails(this.get('id'), this.get('nodeType'));
+                            detailsPanel.show();
+                        }
+
+                        this.el.attr('class', this.svgClassName());
+                    },
                     onExit: function () {
                         var node = this.el;
                         node.select('use')
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js
new file mode 100644
index 0000000..9673600
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2016-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.
+ */
+
+/*
+ ONOS GUI -- Topology View Module.
+ Module that displays the details panel for selected nodes
+ */
+
+(function () {
+    'use strict';
+
+    // Injected Services
+    var Panel, gs, wss, flash, bs, fs, ns;
+
+    // Internal State
+    var detailsPanel;
+
+    // configuration
+    var id = 'topo2-p-detail',
+        className = 'topo-p',
+        panelOpts = {
+            width: 260          // summary and detail panel width
+        },
+        handlerMap = {
+            'showDetails': showDetails
+        };
+
+    var coreButtons = {
+        showDeviceView: {
+            gid: 'switch',
+            tt: 'Show Device View',
+            path: 'device'
+        },
+        showFlowView: {
+            gid: 'flowTable',
+            tt: 'Show Flow View for this Device',
+            path: 'flow'
+        },
+        showPortView: {
+            gid: 'portTable',
+            tt: 'Show Port View for this Device',
+            path: 'port'
+        },
+        showGroupView: {
+            gid: 'groupTable',
+            tt: 'Show Group View for this Device',
+            path: 'group'
+        },
+        showMeterView: {
+            gid: 'meterTable',
+            tt: 'Show Meter View for this Device',
+            path: 'meter'
+        }
+    };
+
+    function init() {
+
+        bindHandlers();
+
+        var options = angular.extend({}, panelOpts, {
+            class: className
+        });
+
+        detailsPanel = new Panel(id, options);
+        detailsPanel.p.classed(className, true);
+    }
+
+    function addProp(tbody, label, value) {
+        var tr = tbody.append('tr'),
+            lab;
+        if (typeof label === 'string') {
+            lab = label.replace(/_/g, ' ');
+        } else {
+            lab = label;
+        }
+
+        function addCell(cls, txt) {
+            tr.append('td').attr('class', cls).html(txt);
+        }
+        addCell('label', lab + ' :');
+        addCell('value', value);
+    }
+
+    function addSep(tbody) {
+        tbody.append('tr').append('td').attr('colspan', 2).append('hr');
+    }
+
+    function listProps(tbody, data) {
+        data.propOrder.forEach(function (p) {
+            if (p === '-') {
+                addSep(tbody);
+            } else {
+                addProp(tbody, p, data.props[p]);
+            }
+        });
+    }
+
+    function addBtnFooter() {
+        detailsPanel.appendToFooter('hr');
+        detailsPanel.appendToFooter('div').classed('actionBtns', true);
+    }
+
+    function addAction(o) {
+        var btnDiv = d3.select('#' + id)
+            .select('.actionBtns')
+            .append('div')
+            .classed('actionBtn', true);
+        bs.button(btnDiv, id + '-' + o.id, o.gid, o.cb, o.tt);
+    }
+
+    function installButtons(buttons, data, devId) {
+        buttons.forEach(function (id) {
+            var btn = coreButtons[id],
+                gid = btn && btn.gid,
+                tt = btn && btn.tt,
+                path = btn && btn.path;
+
+            if (btn) {
+                addAction({
+                    id: 'core-' + id,
+                    gid: gid,
+                    tt: tt,
+                    cb: function () { ns.navTo(path, { devId: devId }); }
+                });
+            }
+            // else if (btn = _getButtonDef(id, data)) {
+            //     addAction(btn);
+            // }
+        });
+    }
+
+    function renderSingle(data) {
+
+        detailsPanel.emptyRegions();
+
+        var svg = detailsPanel.appendToHeader('div')
+                .classed('icon clickable', true)
+                .append('svg'),
+            title = detailsPanel.appendToHeader('h2')
+                .classed('clickable', true),
+            table = detailsPanel.appendToBody('table'),
+            tbody = table.append('tbody'),
+            navFn;
+
+        gs.addGlyph(svg, (data.type || 'unknown'), 26);
+        title.text(data.title);
+
+        // // only add navigation when displaying a device
+        // if (isDevice[data.type]) {
+        //     navFn = function () {
+        //         ns.navTo(devPath, { devId: data.id });
+        //     };
+        //
+        //     svg.on('click', navFn);
+        //     title.on('click', navFn);
+        // }
+
+        listProps(tbody, data);
+        addBtnFooter();
+    }
+
+
+    function bindHandlers() {
+        wss.bindHandlers(handlerMap);
+    }
+
+    function updateDetails(id, nodeType) {
+        wss.sendEvent('requestDetails', {
+            id: id,
+            class: nodeType
+        });
+    }
+
+    function showDetails(data) {
+        var buttons = fs.isA(data.buttons) || [];
+        renderSingle(data);
+        installButtons(buttons, data, data.id);
+    }
+
+    function toggle() {
+        var on = detailsPanel.p.toggle(),
+            verb = on ? 'Show' : 'Hide';
+        flash.flash(verb + ' Summary Panel');
+    }
+
+    function show() {
+        detailsPanel.p.show();
+    }
+
+    function hide() {
+        detailsPanel.p.hide();
+    }
+
+    function destroy() {
+        wss.unbindHandlers(handlerMap);
+        detailsPanel.destroy();
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2DeviceDetailsPanel',
+    ['Topo2PanelService', 'GlyphService', 'WebSocketService', 'FlashService',
+    'ButtonService', 'FnService', 'NavService',
+        function (_ps_, _gs_, _wss_, _flash_, _bs_, _fs_, _ns_) {
+
+            Panel = _ps_;
+            gs = _gs_;
+            wss = _wss_;
+            flash = _flash_;
+            bs = _bs_;
+            fs = _fs_;
+            ns = _ns_;
+
+            return {
+                init: init,
+                updateDetails: updateDetails,
+
+                toggle: toggle,
+                show: show,
+                hide: hide,
+                destroy: destroy
+            };
+        }
+    ]);
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
index 33c4183..89eb020 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
@@ -190,18 +190,49 @@
 
     // ========================== Main Service Definition
 
+    function update(elements) {
+        angular.forEach(elements, function (el) {
+            el.update();
+        });
+    }
+
     function updateNodes() {
-        var allNodes = t2rs.regionNodes();
-        angular.forEach(allNodes, function (node) {
-            node.update();
+        update(t2rs.regionNodes());
+    }
+
+    function updateLinks() {
+        update(t2rs.regionLinks());
+    }
+
+    function resetAllLocations() {
+        var nodes = t2rs.regionNodes();
+
+        angular.forEach(nodes, function (node) {
+            node.resetPosition();
+        });
+
+        t2ls.update();
+        t2ls.tick();
+    }
+
+    function unpin() {
+        var hovered = t2rs.filterRegionNodes(function (model) {
+            return model.get('hovered');
+        });
+
+        angular.forEach(hovered, function (model) {
+            model.fixed = false;
+            model.el.classed('fixed', false);
         });
     }
 
     angular.module('ovTopo2')
     .factory('Topo2ForceService',
-        ['$log', 'WebSocketService', 'Topo2InstanceService', 'Topo2RegionService',
-        'Topo2LayoutService', 'Topo2ViewService', 'Topo2BreadcrumbService', 'Topo2ZoomService',
-        function (_$log_, _wss_, _t2is_, _t2rs_, _t2ls_, _t2vs_, _t2bcs_, zoomService) {
+        ['$log', 'WebSocketService', 'Topo2InstanceService',
+        'Topo2RegionService', 'Topo2LayoutService', 'Topo2ViewService',
+        'Topo2BreadcrumbService', 'Topo2ZoomService',
+        function (_$log_, _wss_, _t2is_, _t2rs_, _t2ls_,
+            _t2vs_, _t2bcs_, zoomService) {
 
             $log = _$log_;
             wss = _wss_;
@@ -238,7 +269,10 @@
                 showMastership: showMastership,
                 topo2PeerRegions: topo2PeerRegions,
 
-                updateNodes: updateNodes
+                updateNodes: updateNodes,
+                updateLinks: updateLinks,
+                resetAllLocations: resetAllLocations,
+                unpin: unpin
             };
         }]);
 })();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2KeyCommands.js b/web/gui/src/main/webapp/app/view/topo2/topo2KeyCommands.js
index 8571575..8885a89 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2KeyCommands.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2KeyCommands.js
@@ -17,9 +17,9 @@
 (function () {
 
     // Injected Services
-    var ks, t2ps, t2ms, ps, t2is, t2sp;
+    var ks, flash, wss, t2ps, t2ms, ps, t2is, t2sp, t2vs;
 
-    var topo2ForceService;
+    var t2fs;
 
     // Commmands
     var actionMap = {
@@ -27,11 +27,16 @@
         G: [openMapSelection, 'Select background geo map'],
         B: [toggleMap, 'Toggle background geo map'],
         I: [toggleInstancePanel, 'Toggle ONOS Instance Panel'],
-        O: [toggleSummary, 'Toggle the Summary Panel']
+        O: [toggleSummary, 'Toggle the Summary Panel'],
+        R: [resetZoom, 'Reset pan / zoom'],
+        P: [togglePorts, 'Toggle Port Highlighting'],
+        E: [equalizeMasters, 'Equalize mastership roles'],
+        X: [resetAllNodeLocations, 'Reset Node Location'],
+        U: [unpinNode, 'Unpin node (mouse over)']
     };
 
-    function init(t2fs) {
-        topo2ForceService = t2fs;
+    function init(_t2fs_) {
+        t2fs = _t2fs_;
         bindCommands();
     }
 
@@ -58,7 +63,7 @@
     function cycleDeviceLabels() {
         var deviceLabelIndex = t2ps.get('dlbls') + 1;
         t2ps.set('dlbls', deviceLabelIndex % 3);
-        topo2ForceService.updateNodes();
+        t2fs.updateNodes();
     }
 
     function openMapSelection() {
@@ -77,17 +82,47 @@
         t2sp.toggle();
     }
 
+    function resetZoom() {
+        t2ms.resetZoom();
+        flash.flash('Pan and zoom reset');
+    }
+
+    function togglePorts(x) {
+        updatePrefsState('porthl', t2vs.togglePortHighlights(x));
+        t2fs.updateLinks();
+    }
+
+    function equalizeMasters() {
+        wss.sendEvent('equalizeMasters');
+        flash.flash('Equalizing master roles');
+    }
+
+    function resetAllNodeLocations() {
+        t2fs.resetAllLocations();
+        flash.flash('Reset node locations');
+    }
+
+    function unpinNode() {
+        t2fs.unpin();
+        flash.flash('Unpin node');
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2KeyCommandService',
-    ['KeyService', 'Topo2PrefsService', 'Topo2MapService', 'PrefsService',
-    'Topo2InstanceService', 'Topo2SummaryPanelService',
-        function (_ks_, _t2ps_, _t2ms_, _ps_, _t2is_, _t2sp_) {
+    ['KeyService', 'FlashService', 'WebSocketService', 'Topo2PrefsService',
+    'Topo2MapService', 'PrefsService', 'Topo2InstanceService',
+    'Topo2SummaryPanelService', 'Topo2ViewService',
+        function (_ks_, _flash_, _wss_, _t2ps_, _t2ms_, _ps_, _t2is_, _t2sp_, _t2vs_) {
+
+            ks = _ks_;
+            flash = _flash_;
+            wss = _wss_;
             t2ps = _t2ps_;
             t2ms = _t2ms_;
             t2is = _t2is_;
             ps = _ps_;
-            ks = _ks_;
             t2sp = _t2sp_;
+            t2vs = _t2vs_;
 
             return {
                 init: init,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
index a02a844..4cd86cc 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -438,6 +438,7 @@
                     init: init,
                     createForceLayout: createForceLayout,
                     update: update,
+                    tick: tick,
                     start: start,
 
                     setDimensions: setDimensions
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
index c62a935..415a21e 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -22,8 +22,7 @@
 (function () {
     'use strict';
 
-    var $log;
-    var Collection, Model, ts, sus, t2zs;
+    var $log, Collection, Model, ts, sus, t2zs, t2vs;
 
     var linkLabelOffset = '0.35em';
 
@@ -59,7 +58,6 @@
         var attrs = angular.extend({}, linkPoints, {
             key: this.get('id'),
             class: 'link',
-            weight: 1,
             srcPort: this.get('srcPort'),
             tgtPort: this.get('dstPort'),
             position: {
@@ -144,44 +142,47 @@
                 });
 
                 this.el.classed('enhanced', true);
-                point = this.locatePortLabel();
-                angular.extend(point, {
-                    id: 'topo-port-tgt',
-                    num: this.get('portB')
-                });
-                data.push(point);
 
-                if (this.get('portA')) {
-                    point = this.locatePortLabel(1);
+                if (showPort()) {
+                    point = this.locatePortLabel();
                     angular.extend(point, {
-                        id: 'topo-port-src',
-                        num: this.get('portA')
+                        id: 'topo-port-tgt',
+                        num: this.get('portB')
                     });
                     data.push(point);
+
+                    if (this.get('portA')) {
+                        point = this.locatePortLabel(1);
+                        angular.extend(point, {
+                            id: 'topo-port-src',
+                            num: this.get('portA')
+                        });
+                        data.push(point);
+                    }
+
+                    var entering = d3.select('#topo-portLabels')
+                        .selectAll('.portLabel')
+                        .data(data).enter().append('g')
+                        .classed('portLabel', true)
+                        .attr('id', function (d) { return d.id; });
+
+                    entering.each(function (d) {
+                        var el = d3.select(this),
+                            rect = el.append('rect'),
+                            text = el.append('text').text(d.num);
+
+                        var rectSize = rectAroundText(el);
+
+                        rect.attr(rectSize)
+                            .attr('rx', 2)
+                            .attr('ry', 2);
+
+                        text.attr('dy', linkLabelOffset)
+                            .attr('text-anchor', 'middle');
+
+                        el.attr('transform', sus.translate(d.x, d.y));
+                    });
                 }
-
-                var entering = d3.select('#topo-portLabels')
-                    .selectAll('.portLabel')
-                    .data(data).enter().append('g')
-                    .classed('portLabel', true)
-                    .attr('id', function (d) { return d.id; });
-
-                entering.each(function (d) {
-                    var el = d3.select(this),
-                        rect = el.append('rect'),
-                        text = el.append('text').text(d.num);
-
-                    var rectSize = rectAroundText(el);
-
-                    rect.attr(rectSize)
-                        .attr('rx', 2)
-                        .attr('ry', 2);
-
-                    text.attr('dy', linkLabelOffset)
-                        .attr('text-anchor', 'middle');
-
-                    el.attr('transform', sus.translate(d.x, d.y));
-                });
             },
             unenhance: function () {
                 this.el.classed('enhanced', false);
@@ -248,6 +249,11 @@
             setScale: function () {
                 var width = linkScale(widthRatio / t2zs.scale());
                 this.el.style('stroke-width', width + 'px');
+            },
+            update: function () {
+                if (this.el.classed('enhanced')) {
+                    this.enhance();
+                }
             }
         });
 
@@ -258,17 +264,23 @@
         return new LinkCollection(data);
     }
 
+    function showPort() {
+        return t2vs.getPortHighlighting();
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2LinkService',
         ['$log', 'Topo2Collection', 'Topo2Model',
         'ThemeService', 'SvgUtilService', 'Topo2ZoomService',
-
-            function (_$log_, _Collection_, _Model_, _ts_, _sus_, _t2zs_) {
+        'Topo2ViewService',
+            function (_$log_, _Collection_, _Model_, _ts_, _sus_,
+                _t2zs_, _t2vs_) {
 
                 $log = _$log_;
                 ts = _ts_;
                 sus = _sus_;
                 t2zs = _t2zs_;
+                t2vs = _t2vs_;
                 Collection = _Collection_;
                 Model = _Model_;
 
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Map.js b/web/gui/src/main/webapp/app/view/topo2/topo2Map.js
index 4ff2162..ce7e956 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Map.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Map.js
@@ -29,13 +29,15 @@
     var MapSelectionDialog;
 
     // internal state
-    var mapG;
+    var mapG, zoomLayer, zoomer;
 
-    function init(zoomLayer) {
-        return setUpMap(zoomLayer);
+    function init(_zoomLayer_, _zoomer_) {
+        zoomLayer = _zoomLayer_;
+        zoomer = _zoomer_;
+        return setUpMap();
     }
 
-    function setUpMap(zoomLayer) {
+    function setUpMap() {
         var prefs = currentMap(),
             mapId = prefs.mapid,
             mapFilePath = prefs.mapfilepath,
@@ -124,6 +126,10 @@
         }).open();
     }
 
+    function resetZoom() {
+        zoomer.reset();
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2MapService',
         ['$location', 'PrefsService', 'MapService',
@@ -140,7 +146,9 @@
                 return {
                     init: init,
                     openMapSelection: openMapSelection,
-                    toggle: toggle
+                    toggle: toggle,
+
+                    resetZoom: resetZoom
                 };
             }
         ]);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
index 109a694..42275bf 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
@@ -15,18 +15,14 @@
  */
 
 /*
- ONOS GUI -- Topology Layout Module.
- Module that contains the d3.force.layout logic
+ ONOS GUI -- Topology Node Module.
+ Module that contains model for nodes within the topology
  */
 
 (function () {
     'use strict';
 
-    var randomService, ps, sus, is, ts, t2mcs;
-    var fn;
-
-    // Internal state;
-    var nearDist = 15;
+    var ps, sus, is, ts, t2mcs, t2nps, fn;
 
     var devIconDim = 36,
         devIconDimMin = 20,
@@ -56,133 +52,56 @@
             dColTheme[ts.theme()][otag];
     }
 
-    function positionNode(node, forUpdate) {
-        var meta = node.get('metaUi'),
-            x = meta && meta.x,
-            y = meta && meta.y,
-            dim = [800, 600],
-            xy;
-
-        // If the device contains explicit LONG/LAT data, use that to position
-        if (setLongLat(node)) {
-            // Indicate we want to update cached meta data...
-            return true;
-        }
-
-        // else if we have [x,y] cached in meta data, use that...
-        if (x !== undefined && y !== undefined) {
-            node.fixed = true;
-            node.px = node.x = x;
-            node.py = node.y = y;
-            return;
-        }
-
-        // if this is a node update (not a node add).. skip randomizer
-        if (forUpdate) {
-            return;
-        }
-
-        // Note: Placing incoming unpinned nodes at exactly the same point
-        //        (center of the view) causes them to explode outwards when
-        //        the force layout kicks in. So, we spread them out a bit
-        //        initially, to provide a more serene layout convergence.
-        //       Additionally, if the node is a host, we place it near
-        //        the device it is connected to.
-
-        function rand() {
-            return {
-                x: randomService.randDim(dim[0]),
-                y: randomService.randDim(dim[1])
-            };
-        }
-
-        function near(node) {
-            return {
-                x: node.x + nearDist + randomService.spread(nearDist),
-                y: node.y + nearDist + randomService.spread(nearDist)
-            };
-        }
-
-        function getDevice(cp) {
-            return rand();
-        }
-
-        xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
-
-        if (node.class === 'sub-region') {
-            xy = rand();
-            node.x = node.px = xy.x;
-            node.y = node.py = xy.y;
-        }
-        angular.extend(node, xy);
-    }
-
-    function setLongLat(el) {
-        var loc = el.get('location'),
-            coord;
-
-        if (loc && loc.type === 'lnglat') {
-
-            if (loc.lat === 0 && loc.lng === 0) {
-                return false;
-            }
-
-            coord = coordFromLngLat(loc);
-            el.fixed = true;
-            el.x = el.px = coord[0];
-            el.y = el.py = coord[1];
-
-            return true;
-        }
-    }
-
-    function coordFromLngLat(loc) {
-        var p = t2mcs.projection();
-        return p ? p([loc.lng, loc.lat]) : [0, 0];
-    }
-
     angular.module('ovTopo2')
     .factory('Topo2NodeModel',
-        ['Topo2Model', 'FnService', 'RandomService', 'Topo2PrefsService',
+        ['Topo2Model', 'FnService', 'Topo2PrefsService',
         'SvgUtilService', 'IconService', 'ThemeService',
-        'Topo2MapConfigService', 'Topo2ZoomService',
-        function (Model, _fn_, _RandomService_, _ps_, _sus_, _is_, _ts_,
-            _t2mcs_, zoomService) {
+        'Topo2MapConfigService', 'Topo2ZoomService', 'Topo2NodePositionService',
+        function (Model, _fn_, _ps_, _sus_, _is_, _ts_,
+            _t2mcs_, zoomService, _t2nps_) {
 
-            randomService = _RandomService_;
             ts = _ts_;
             fn = _fn_;
             ps = _ps_;
             sus = _sus_;
             is = _is_;
             t2mcs = _t2mcs_;
+            t2nps = _t2nps_;
 
             return Model.extend({
                 initialize: function () {
-                    this.set('class', this.nodeType);
-                    this.set('svgClass', this.svgClassName());
                     this.node = this.createNode();
+                    this._events = {
+                        'mouseover': 'mouseoverHandler',
+                        'mouseout': 'mouseoutHandler'
+                    };
                 },
                 createNode: function () {
-                    this.set('class', this.nodeType);
                     this.set('svgClass', this.svgClassName());
-                    positionNode(this);
+                    t2nps.positionNode(this);
                     return this;
                 },
                 setUpEvents: function () {
-                    var _this = this;
-                    angular.forEach(this.events, function (handler, key) {
+                    var _this = this,
+                        events = angular.extend({}, this._events, this.events);
+                    angular.forEach(events, function (handler, key) {
                         _this.el.on(key, _this[handler].bind(_this));
                     });
                 },
+                mouseoverHandler: function () {
+                    this.set('hovered', true);
+                },
+                mouseoutHandler: function () {
+                    this.set('hovered', false);
+                },
                 icon: function () {
                     return 'unknown';
                 },
                 label: function () {
                     var props = this.get('props'),
                         id = this.get('id'),
-                        friendlyName = props ? props.name : id,
-                        labels = ['', friendlyName, id],
+                        friendlyName = props && props.name ? props.name : id,
+                        labels = ['', friendlyName || id, id],
                         nli = ps.get('dlbls'),
                         idx = (nli < labels.length) ? nli : 0;
 
@@ -197,7 +116,8 @@
                     return box.width + labelPad * 2;
                 },
                 addLabelElements: function (label) {
-                    var rect = this.el.append('rect');
+                    var rect = this.el.append('rect')
+                        .attr('class', 'node-container');
                     var glythRect = this.el.append('rect')
                         .attr('y', -halfDevIcon)
                         .attr('x', -halfDevIcon)
@@ -243,7 +163,8 @@
                         this.nodeType,
                         this.get('type'),
                         {
-                            online: this.get('online')
+                            online: this.get('online'),
+                            selected: this.get('selected')
                         }
                     );
                 },
@@ -251,6 +172,9 @@
                     var p = t2mcs.projection();
                     return p ? p.invert(coord) : [0, 0];
                 },
+                resetPosition: function () {
+                    t2nps.setLongLat(this);
+                },
                 update: function () {
                     this.updateLabel();
                 },
@@ -281,8 +205,8 @@
                         multipler = devIconDimMax / (dim * zoomService.scale());
                     }
 
-
-                    this.el.selectAll('*').style('transform', 'scale(' + multipler + ')');
+                    this.el.selectAll('*')
+                        .style('transform', 'scale(' + multipler + ')');
                 },
                 render: function () {
                     var node = this.el,
@@ -293,7 +217,8 @@
                     // Label
                     var labelElements = this.addLabelElements(label);
                     labelWidth = label ? this.computeLabelWidth(node) : 0;
-                    labelElements.rect.attr(this.labelBox(devIconDim, labelWidth));
+                    labelElements.rect
+                        .attr(this.labelBox(devIconDim, labelWidth));
 
                     // Icon
                     glyph = is.addDeviceIcon(node, glyphId, devIconDim);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2NodePosition.js b/web/gui/src/main/webapp/app/view/topo2/topo2NodePosition.js
new file mode 100644
index 0000000..7381f6b
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodePosition.js
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2016-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.
+ */
+
+/*
+ ONOS GUI -- Topology Node Position Module.
+ Module that helps position nodes in the topology
+ */
+
+(function () {
+    'use strict';
+
+    // Injected vars
+    var rs, t2mcs;
+
+    // Internal state;
+    var nearDist = 15;
+
+    function positionNode(node, forUpdate) {
+        var meta = node.get('metaUi'),
+            x = meta && meta.x,
+            y = meta && meta.y,
+            dim = [800, 600],
+            xy;
+
+        // If the device contains explicit LONG/LAT data, use that to position
+        if (setLongLat(node)) {
+            // Indicate we want to update cached meta data...
+            return true;
+        }
+
+        // else if we have [x,y] cached in meta data, use that...
+        if (x !== undefined && y !== undefined) {
+            node.fixed = true;
+            node.px = node.x = x;
+            node.py = node.y = y;
+            return;
+        }
+
+        // if this is a node update (not a node add).. skip randomizer
+        if (forUpdate) {
+            return;
+        }
+
+        // Note: Placing incoming unpinned nodes at exactly the same point
+        //        (center of the view) causes them to explode outwards when
+        //        the force layout kicks in. So, we spread them out a bit
+        //        initially, to provide a more serene layout convergence.
+        //       Additionally, if the node is a host, we place it near
+        //        the device it is connected to.
+
+        function rand() {
+            return {
+                x: rs.randDim(dim[0]),
+                y: rs.randDim(dim[1])
+            };
+        }
+
+        function near(node) {
+            return {
+                x: node.x + nearDist + rs.spread(nearDist),
+                y: node.y + nearDist + rs.spread(nearDist)
+            };
+        }
+
+        function getDevice(cp) {
+            return rand();
+        }
+
+        xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
+
+        if (node.class === 'sub-region') {
+            xy = rand();
+            node.x = node.px = xy.x;
+            node.y = node.py = xy.y;
+        }
+        angular.extend(node, xy);
+    }
+
+    function setLongLat(el) {
+        var loc = el.get('location'),
+            coord;
+
+        if (loc && loc.type === 'lnglat') {
+
+            if (loc.lat === 0 && loc.lng === 0) {
+                return false;
+            }
+
+            coord = coordFromLngLat(loc);
+            el.fixed = true;
+            el.x = el.px = coord[0];
+            el.y = el.py = coord[1];
+
+            return true;
+        }
+    }
+
+    function coordFromLngLat(loc) {
+        var p = t2mcs.projection();
+        return p ? p([loc.lng, loc.lat]) : [0, 0];
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2NodePositionService',
+        ['RandomService',
+            function (_rs_, _t2mcs_) {
+
+                rs = _rs_;
+                t2mcs = _t2mcs_;
+
+                return {
+                    positionNode: positionNode,
+                    setLongLat: setLongLat
+                };
+            }
+        ]);
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Panel.js b/web/gui/src/main/webapp/app/view/topo2/topo2Panel.js
index bfd2a3f..5836382 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Panel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Panel.js
@@ -29,7 +29,9 @@
         this.p = ps.createPanel(this.id, options);
         this.setup();
 
-        this.p.show();
+        if (options.show) {
+            this.p.show();
+        }
     };
 
     Panel.prototype = {
@@ -59,8 +61,8 @@
             this.body.selectAll("*").remove();
             this.footer.selectAll("*").remove();
         },
-        destory: function () {
-            ps.destroy(this.id);
+        destroy: function () {
+            ps.destroyPanel(this.id);
         }
     };
 
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
index 3f6fa17..6becc49 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -80,6 +80,11 @@
         return [];
     }
 
+    function filterRegionNodes(predicate) {
+        var nodes = regionNodes();
+        return _.filter(nodes, predicate);
+    }
+
     function regionLinks() {
         return (region) ? region.get('links').models : [];
     }
@@ -105,6 +110,7 @@
                 addRegion: addRegion,
                 regionNodes: regionNodes,
                 regionLinks: regionLinks,
+                filterRegionNodes: filterRegionNodes,
 
                 getSubRegions: t2sr.getSubRegions
             };
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2SummaryPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2SummaryPanel.js
index 149e7cc..ac12c2b 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2SummaryPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SummaryPanel.js
@@ -29,8 +29,12 @@
     var summaryPanel, summaryData;
 
     // configuration
-    var id = 'topo-p-summary',
+    var id = 'topo2-p-summary',
         className = 'topo-p',
+        panelOpts = {
+            show: true,
+            width: 260          // summary and detail panel width
+        },
         handlerMap = {
             showSummary: handleSummaryData
         };
@@ -40,10 +44,12 @@
         bindHandlers();
         wss.sendEvent('requestSummary');
 
-        summaryPanel = new Panel(id, {
+        var options = angular.extend({}, panelOpts, {
             class: className
         });
 
+        summaryPanel = new Panel(id, options);
+
         summaryPanel.p.classed(className, true);
     }
 
@@ -107,6 +113,11 @@
         flash.flash(verb + ' Summary Panel');
     }
 
+    function destroy() {
+        wss.unbindHandlers(handlerMap);
+        summaryPanel.destroy();
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2SummaryPanelService',
     ['Topo2PanelService', 'GlyphService', 'WebSocketService', 'FlashService',
@@ -120,7 +131,8 @@
             return {
                 init: init,
 
-                toggle: toggle
+                toggle: toggle,
+                destroy: destroy
             };
         }
     ]);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2View.js b/web/gui/src/main/webapp/app/view/topo2/topo2View.js
index 9178e1a..ae29894 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2View.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2View.js
@@ -15,14 +15,21 @@
  */
 
 /*
- ONOS GUI -- Topology Layout Module.
- Module that contains the d3.force.layout logic
+ ONOS GUI -- Topology View Module.
+ Module that contains the topology view variables
  */
 
 (function () {
     'use strict';
 
-    var dimensions;
+    // Injected Services
+    var flash;
+
+    // Internal State
+    var dimensions,
+        viewOptions = {
+            linkPortHighlighting: true
+        };
 
     function newDim(_dimensions) {
         dimensions = _dimensions;
@@ -32,13 +39,33 @@
         return dimensions;
     }
 
+    function togglePortHighlights(x) {
+        var kev = (x === 'keyev'),
+            on = kev ? !viewOptions.linkPortHighlighting : Boolean(x),
+            what = on ? 'Enable' : 'Disable';
+
+        viewOptions.linkPortHighlighting = on;
+        flash.flash(what + ' port highlighting');
+        return on;
+    }
+
+    function getPortHighlighting() {
+        return viewOptions.linkPortHighlighting;
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2ViewService',
-        [
-            function () {
+        ['FlashService',
+            function (_flash_) {
+
+                flash = _flash_;
+
                 return {
                     newDim: newDim,
-                    getDimensions: getDimensions
+                    getDimensions: getDimensions,
+
+                    togglePortHighlights: togglePortHighlights,
+                    getPortHighlighting: getPortHighlighting
                 };
             }
         ]