Topo2 Uses multiple ForceG layers for region animations
Topo2 corrected injected deps naming inconsistencies
Topo2 Region navigation comment fixes
Refactored Topo2Layout
Changed SVG 'g' elements to use class names
Center Layout on Region Navigation
Upgraded D3 to patch the force layout end event
Fix - No enhance on link hover if port highlight is disabled
Fix - Link selection labels for A/B Label and A/B Port properties
Refactored Topo2Layout link selection to be part of Topo2SelectService
Linted Topo2 Javascript
Refactored Topo2RegionService

Change-Id: I0e3a22fbc85df99af94fabd3e45191a95ee502b6
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 6845aa0..e37872f 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.js
@@ -79,9 +79,8 @@
     // === Controller Definition -----------------------------------------
 
     angular.module('ovTopo2', ['onosUtil', 'onosSvg', 'onosRemote'])
-    .controller('OvTopo2Ctrl',
-        ['$scope', '$log', '$location',
-        'FnService', 'MastService', 'KeyService',
+    .controller('OvTopo2Ctrl', [
+        '$scope', '$log', '$location', 'FnService', 'MastService', 'KeyService',
         'GlyphService', 'MapService', 'SvgUtilService', 'FlashService',
         'WebSocketService', 'PrefsService', 'ThemeService',
         'Topo2EventService', 'Topo2ForceService', 'Topo2InstanceService',
@@ -193,9 +192,6 @@
                 }
             );
 
-            // initialize the force layout, ready to render the topology
-            forceG = zoomLayer.append('g').attr('id', 'topo-force');
-
             t2fs.init(svg, forceG, uplink, dim, zoomer);
             t2bcs.init();
             t2kcs.init(t2fs);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Breadcrumb.js b/web/gui/src/main/webapp/app/view/topo2/topo2Breadcrumb.js
index 78e486c..84421c9 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Breadcrumb.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Breadcrumb.js
@@ -27,7 +27,8 @@
 
     // Internal
     var breadcrumbContainer,
-        breadcrumbs;
+        breadcrumbs,
+        layout;
 
     function init() {
         $log.debug("Topo2BreadcrumbService Initiated");
@@ -61,6 +62,9 @@
             rid: data.id
         });
 
+        layout.createForceElements();
+        layout.transitionDownRegion();
+
         render();
     }
 
@@ -101,13 +105,15 @@
             .styleTween('transform', function (d) {
                 return translateInterpolator;
             });
+    }
 
+    function addLayout(_layout_) {
+        layout = _layout_;
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2BreadcrumbService',
-        ['$log', 'WebSocketService',
-
+    .factory('Topo2BreadcrumbService', [
+        '$log', 'WebSocketService',
         function (_$log_, _wss_) {
 
             $log = _$log_;
@@ -116,8 +122,9 @@
             return {
                 init: init,
                 addBreadcrumb: addBreadcrumb,
+                addLayout: addLayout,
                 hide: hide
             };
-        }]);
-
+        }
+    ]);
 })();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
index ab684eb..a0dd430 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
@@ -22,8 +22,7 @@
 (function () {
     'use strict';
 
-    var Model,
-        extend;
+    var Model;
 
     function Collection(models, options) {
 
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2D3.js b/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
index 9123ac7..932101c 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
@@ -39,14 +39,16 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2D3Service',
-    [function (_is_) {
-        return {
-            nodeEnter: nodeEnter,
-            nodeExit: nodeExit,
-            hostEnter: hostEnter,
-            linkEntering: linkEntering
-        };
-    }]
+    .factory('Topo2D3Service', [
+
+        function (_is_) {
+            return {
+                nodeEnter: nodeEnter,
+                nodeExit: nodeExit,
+                hostEnter: hostEnter,
+                linkEntering: linkEntering
+            };
+        }
+    ]
 );
 })();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2DetailsPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2DetailsPanel.js
index 3bea0bc..16ab45b8 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2DetailsPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2DetailsPanel.js
@@ -51,8 +51,8 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2DetailsPanelService',
-    ['Topo2PanelService',
+    .factory('Topo2DetailsPanelService', [
+        'Topo2PanelService',
         function (_ps_) {
 
             Panel = _ps_;
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js
index 34645f8..e8778ed 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2DeviceDetailsPanel.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // Injected Services
-    var Panel, gs, wss, flash, bs, fs, ns, listProps;
+    var panel, gs, wss, flash, bs, fs, ns, ls;
 
     // Internal State
     var detailsPanel;
@@ -65,7 +65,7 @@
     function init() {
 
         bindHandlers();
-        detailsPanel = Panel();
+        detailsPanel = panel();
     }
 
     function addBtnFooter() {
@@ -114,7 +114,7 @@
         gs.addGlyph(svg, (data.type || 'unknown'), 26);
         title.text(data.title);
 
-        listProps(tbody, data);
+        ls.listProps(tbody, data);
         addBtnFooter();
     }
 
@@ -187,19 +187,20 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2DeviceDetailsPanel',
-    ['Topo2DetailsPanelService', 'GlyphService', 'WebSocketService', 'FlashService',
-    'ButtonService', 'FnService', 'NavService', 'ListService', 
-        function (_ps_, _gs_, _wss_, _flash_, _bs_, _fs_, _ns_, _listService_) {
+    .factory('Topo2DeviceDetailsPanel', [
+        'Topo2DetailsPanelService', 'GlyphService', 'WebSocketService', 'FlashService',
+        'ButtonService', 'FnService', 'NavService', 'ListService',
 
-            Panel = _ps_;
+        function (_ps_, _gs_, _wss_, _flash_, _bs_, _fs_, _ns_, _ls_) {
+
+            panel = _ps_;
             gs = _gs_;
             wss = _wss_;
             flash = _flash_;
             bs = _bs_;
             fs = _fs_;
             ns = _ns_;
-            listProps = _listService_;
+            ls = _ls_;
 
             return {
                 init: init,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Dialog.js b/web/gui/src/main/webapp/app/view/topo2/topo2Dialog.js
index 13da1a8..c7661fc 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Dialog.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Dialog.js
@@ -31,8 +31,8 @@
     // ==========================
 
     angular.module('ovTopo2')
-    .factory('Topo2DialogService',
-        ['DialogService',
+    .factory('Topo2DialogService', [
+        'DialogService',
 
         function (ds) {
             return {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Event.js b/web/gui/src/main/webapp/app/view/topo2/topo2Event.js
index 138d866..76a573e 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Event.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Event.js
@@ -84,8 +84,8 @@
     // ========================== Main Service Definition
 
     angular.module('ovTopo2')
-    .factory('Topo2EventService',
-        ['$log', 'WebSocketService', 'Topo2ForceService',
+    .factory('Topo2EventService', [
+        '$log', 'WebSocketService', 'Topo2ForceService',
 
         function (_$log_, _wss_, _t2fs_) {
             $log = _$log_;
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 57a5f89..0c1ffb7 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
@@ -26,7 +26,7 @@
     var $log,
         wss;
 
-    var t2is, t2rs, t2ls, t2vs, t2bcs;
+    var t2is, t2rs, t2ls, t2vs, t2bcs, t2ss;
     var svg, forceG, uplink, dim, opts, zoomer;
 
     // D3 Selections
@@ -34,15 +34,17 @@
 
     // ========================== Helper Functions
 
-    function init(_svg_, _forceG_, _uplink_, _dim_, _zoomer_, _opts_) {
+    function init(_svg_, _forceG_, _uplink_, _dim_, zoomer, _opts_) {
         svg = _svg_;
         forceG = _forceG_;
         uplink = _uplink_;
         dim = _dim_;
         opts = _opts_;
-        zoomer = _zoomer_;
 
-        t2ls.init(svg, forceG, uplink, dim, zoomer, opts);
+        t2ls = t2ls(svg, forceG, uplink, dim, zoomer, opts);
+        t2bcs.addLayout(t2ls);
+        t2rs.layout = t2ls;
+        t2ss.init(svg, zoomer);
     }
 
     function destroy() {
@@ -233,12 +235,12 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2ForceService',
-        ['$log', 'WebSocketService', 'Topo2InstanceService',
+    .factory('Topo2ForceService', [
+        '$log', 'WebSocketService', 'Topo2InstanceService',
         'Topo2RegionService', 'Topo2LayoutService', 'Topo2ViewService',
-        'Topo2BreadcrumbService', 'Topo2ZoomService',
+        'Topo2BreadcrumbService', 'Topo2ZoomService', 'Topo2SelectService',
         function (_$log_, _wss_, _t2is_, _t2rs_, _t2ls_,
-            _t2vs_, _t2bcs_, zoomService) {
+            _t2vs_, _t2bcs_, zoomService, _t2ss_) {
 
             $log = _$log_;
             wss = _wss_;
@@ -247,6 +249,7 @@
             t2ls = _t2ls_;
             t2vs = _t2vs_;
             t2bcs = _t2bcs_;
+            t2ss = _t2ss_;
 
             var onZoom = function () {
                 var nodes = [].concat(
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
index ee13581..40755fe 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
@@ -48,12 +48,12 @@
     angular.module('ovTopo2')
     .factory('Topo2HostService', [
         'Topo2Collection', 'Topo2NodeModel', 'Topo2ViewService',
-        'IconService', 'Topo2ZoomService', 'Topo2HostsPanelService', 
-        function (_Collection_, _NodeModel_, _t2vs_, is, zs, t2hds) {
+        'IconService', 'Topo2ZoomService', 'Topo2HostsPanelService',
+        function (_c_, NodeModel, _t2vs_, is, zs, t2hds) {
 
-            Collection = _Collection_;
+            Collection = _c_;
 
-            Model = _NodeModel_.extend({
+            Model = NodeModel.extend({
                 initialize: function () {
                     this.super = this.constructor.__super__;
                     this.super.initialize.apply(this, arguments);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2HostsPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2HostsPanel.js
index b131729..d6238a2 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2HostsPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2HostsPanel.js
@@ -23,13 +23,13 @@
     'use strict';
 
     // Injected Services
-    var Panel, gs, wss, flash, listProps;
+    var panel, gs, flash, ls;
 
     // Internal State
     var hostPanel, hostData;
 
     function init() {
-        hostPanel = Panel();
+        hostPanel = panel();
     }
 
     function formatHostData(data) {
@@ -40,12 +40,12 @@
                 '-': '',
                 'MAC': data.get('id'),
                 'IP': data.get('ips')[0],
-                'VLAN': 'None', // TODO
+                'VLAN': 'None', // TODO: VLAN is not currently in the data received from backend
                 'Latitude': data.get('location').lat,
-                'Longitude': data.get('location').lng,
+                'Longitude': data.get('location').lng
             }
-        }
-    };
+        };
+    }
 
     function displayPanel(data) {
         init();
@@ -67,7 +67,7 @@
 
         title.text(hostData.title);
         gs.addGlyph(svg, 'bird', 24, 0, [1, 1]);
-        listProps(tbody, hostData);
+        ls.listProps(tbody, hostData);
     }
 
     function show() {
@@ -89,15 +89,14 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2HostsPanelService',
-    ['Topo2DetailsPanelService', 'GlyphService', 'WebSocketService', 'FlashService', 'ListService',
-        function (_ps_, _gs_, _wss_, _flash_, _listService_) {
+    .factory('Topo2HostsPanelService', [
+        'Topo2DetailsPanelService', 'GlyphService', 'FlashService', 'ListService',
+        function (_ps_, _gs_, _wss_, _flash_, _ls_) {
 
-            Panel = _ps_;
+            panel = _ps_;
             gs = _gs_;
-            wss = _wss_;
             flash = _flash_;
-            listProps = _listService_;
+            ls = _ls_;
 
             return {
                 displayPanel: displayPanel,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Instance.js b/web/gui/src/main/webapp/app/view/topo2/topo2Instance.js
index abf5d11..307c2db 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Instance.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Instance.js
@@ -269,25 +269,26 @@
     }
 
     angular.module('ovTopo2')
-        .factory('Topo2InstanceService',
-        ['$log', 'PanelService', 'SvgUtilService', 'GlyphService', 'FlashService',
-        'ThemeService',
+        .factory('Topo2InstanceService', [
+            '$log', 'PanelService', 'SvgUtilService', 'GlyphService',
+            'FlashService', 'ThemeService',
 
-        function (_$log_, _ps_, _sus_, _gs_, _flash_, _ts_) {
-            $log = _$log_;
-            ps = _ps_;
-            sus = _sus_;
-            gs = _gs_;
-            flash = _flash_;
-            ts = _ts_;
+            function (_$log_, _ps_, _sus_, _gs_, _flash_, _ts_) {
+                $log = _$log_;
+                ps = _ps_;
+                sus = _sus_;
+                gs = _gs_;
+                flash = _flash_;
+                ts = _ts_;
 
-            return {
-                initInst: initInst,
-                allInstances: allInstances,
-                destroy: destroy,
-                toggle: toggle,
-                isVisible: function () { return oiBox.isVisible(); }
-            };
-        }]);
+                return {
+                    initInst: initInst,
+                    allInstances: allInstances,
+                    destroy: destroy,
+                    toggle: toggle,
+                    isVisible: function () { return oiBox.isVisible(); }
+                };
+            }
+        ]);
 
 })();
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 02aa984..4f65679 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2KeyCommands.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2KeyCommands.js
@@ -91,7 +91,7 @@
 
     function cycleDeviceLabels() {
         var deviceLabelIndex = t2ps.get('dlbls') + 1,
-            newDeviceLabelIndex =  deviceLabelIndex % 3;
+            newDeviceLabelIndex = deviceLabelIndex % 3;
 
         t2ps.set('dlbls', newDeviceLabelIndex);
         t2fs.updateNodes();
@@ -140,13 +140,12 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2KeyCommandService',
-    ['KeyService', 'FlashService', 'WebSocketService', 'Topo2PrefsService',
-    'Topo2MapService', 'PrefsService', 'Topo2InstanceService',
-    'Topo2SummaryPanelService', 'Topo2DeviceDetailsPanel', 'Topo2ViewService',
-    'Topo2RegionService',
+    .factory('Topo2KeyCommandService', [
+        'KeyService', 'FlashService', 'WebSocketService', 'Topo2PrefsService',
+        'Topo2MapService', 'PrefsService', 'Topo2InstanceService',
+        'Topo2SummaryPanelService', 'Topo2ViewService', 'Topo2RegionService',
         function (_ks_, _flash_, _wss_, _t2ps_, _t2ms_, _ps_, _t2is_, _t2sp_,
-                  _t2ddp_, _t2vs_, _t2rs_) {
+                  _t2vs_, _t2rs_) {
 
             ks = _ks_;
             flash = _flash_;
@@ -156,7 +155,6 @@
             t2is = _t2is_;
             ps = _ps_;
             t2sp = _t2sp_;
-            t2ddp = _t2ddp_;
             t2vs = _t2vs_;
             t2rs = _t2rs_;
 
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 6e5df6c..9849393 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -22,10 +22,7 @@
 (function () {
     'use strict';
 
-    var $log, wss, sus, t2rs, t2d3, t2vs, t2ss;
-
-    var linkG, linkLabelG, nodeG;
-    var link, node, zoomer;
+    var instance;
 
     // default settings for force layout
     var defaultSettings = {
@@ -73,246 +70,14 @@
     };
 
     // internal state
-    var settings,               // merged default settings and options
-        force,                  // force layout object
-        drag,                   // drag behavior handler
-        previousNearestLink,    // previous link to mouse position
-        nodeLock = false;       // whether nodes can be dragged or not (locked)
-
-
-    function init(_svg_, forceG, _uplink_, _dim_, _zoomer_, opts) {
-
-        $log.debug("Initialising Topology Layout");
-        settings = angular.extend({}, defaultSettings, opts);
-
-        linkG = forceG.append('g').attr('id', 'topo-links');
-        linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
-        forceG.append('g').attr('id', 'topo-numLinkLabels');
-        nodeG = forceG.append('g').attr('id', 'topo-nodes');
-        forceG.append('g').attr('id', 'topo-portLabels');
-
-        link = linkG.selectAll('.link');
-        linkLabelG.selectAll('.linkLabel');
-        node = nodeG.selectAll('.node');
-
-        zoomer = _zoomer_;
-        _svg_.on('mousemove', mouseMoveHandler);
-        _svg_.on('click', mouseClickHandler);
-    }
-
-    function getDeviceChargeForType(node) {
-
-        var nodeType = node.get('nodeType');
-
-        return settings.charge[nodeType] ||
-            settings.charge._def_;
-    }
-
-    function getLinkDistanceForLinkType(node) {
-        var nodeType = node.get('type');
-
-        return settings.linkDistance[nodeType] ||
-            settings.linkDistance._def_;
-    }
-
-    function getLinkStrenghForLinkType(node) {
-        var nodeType = node.get('type');
-
-        return settings.linkStrength[nodeType] ||
-            settings.linkStrength._def_;
-    }
-
-    function createForceLayout() {
-
-        var regionLinks = t2rs.regionLinks(),
-            regionNodes = t2rs.regionNodes();
-
-        force = d3.layout.force()
-            .size(t2vs.getDimensions())
-            .gravity(settings.gravity)
-            .friction(settings.friction)
-            .charge(getDeviceChargeForType)
-            .linkDistance(getLinkDistanceForLinkType)
-            .linkStrength(getLinkStrenghForLinkType)
-            .on("tick", tick);
-
-        force
-            .nodes(t2rs.regionNodes())
-            .links(regionLinks)
-            .start();
-
-        link = linkG.selectAll('.link')
-            .data(regionLinks, function (d) { return d.get('key'); });
-
-        node = nodeG.selectAll('.node')
-            .data(regionNodes, function (d) { return d.get('id'); });
-
-        drag = sus.createDragBehavior(force,
-          t2ss.selectObject, atDragEnd, dragEnabled, clickEnabled);
-
-        update();
-    }
+    var nodeLock = false;       // whether nodes can be dragged or not (locked)
 
     // predicate that indicates when clicking is active
     function clickEnabled() {
         return true;
     }
 
-    function zoomingOrPanning(ev) {
-        return ev.metaKey || ev.altKey;
-    }
-
-    function atDragEnd(d) {
-        // once we've finished moving, pin the node in position
-        d.fixed = true;
-        d3.select(this).classed('fixed', true);
-        sendUpdateMeta(d);
-        $log.debug(d);
-        t2ss.clickConsumed(true);
-    }
-
-    // predicate that indicates when dragging is active
-    function dragEnabled() {
-        var ev = d3.event.sourceEvent;
-        // nodeLock means we aren't allowing nodes to be dragged...
-        return !nodeLock && !zoomingOrPanning(ev);
-    }
-
-    function sendUpdateMeta(d, clearPos) {
-        var metaUi = {},
-            ll;
-
-        // if we are not clearing the position data (unpinning),
-        // attach the x, y, (and equivalent longitude, latitude)...
-        if (!clearPos) {
-            ll = d.lngLatFromCoord([d.x, d.y]);
-            metaUi = {
-                x: d.x,
-                y: d.y,
-                equivLoc: {
-                    lng: ll[0],
-                    lat: ll[1]
-                }
-            };
-        }
-        d.metaUi = metaUi;
-        wss.sendEvent('updateMeta2', {
-            id: d.get('id'),
-            class: d.get('class'),
-            memento: metaUi
-        });
-    }
-
-    function tick() {
-        link
-            .attr("x1", function (d) { return d.source.x; })
-            .attr("y1", function (d) { return d.source.y; })
-            .attr("x2", function (d) { return d.target.x; })
-            .attr("y2", function (d) { return d.target.y; });
-
-        node
-            .attr({
-                transform: function (d) {
-                    var dx = isNaN(d.x) ? 0 : d.x,
-                        dy = isNaN(d.y) ? 0 : d.y;
-                    return sus.translate(dx, dy);
-                }
-            });
-    }
-
-    function update() {
-        _updateNodes();
-        _updateLinks();
-    }
-
-    function _updateNodes() {
-
-        var regionNodes = t2rs.regionNodes();
-
-        // select all the nodes in the layout:
-        node = nodeG.selectAll('.node')
-            .data(regionNodes, function (d) { return d.get('id'); });
-
-        var entering = node.enter()
-            .append('g')
-            .attr({
-                id: function (d) { return sus.safeId(d.get('id')); },
-                class: function (d) { return d.svgClassName(); },
-                transform: function (d) {
-                    // Need to guard against NaN here ??
-                    return sus.translate(d.node.x, d.node.y);
-                },
-                opacity: 0
-            })
-            .call(drag)
-            .transition()
-            .attr('opacity', 1);
-
-        entering.filter('.device').each(t2d3.nodeEnter);
-        entering.filter('.sub-region').each(t2d3.nodeEnter);
-        entering.filter('.host').each(t2d3.hostEnter);
-
-        // operate on exiting nodes:
-        // Note that the node is removed after 2 seconds.
-        // Sub element animations should be shorter than 2 seconds.
-        var exiting = node.exit()
-            .transition()
-            .duration(300)
-            .style('opacity', 0)
-            .remove();
-
-        // exiting node specifics:
-        // exiting.filter('.host').each(t2d3.hostExit);
-        exiting.filter('.device').each(t2d3.nodeExit);
-    }
-
-    function _updateLinks() {
-
-        // var th = ts.theme();
-        var regionLinks = t2rs.regionLinks();
-
-        link = linkG.selectAll('.link')
-            .data(regionLinks, function (d) { return d.get('key'); });
-
-        // operate on entering links:
-        var entering = link.enter()
-            .append('line')
-            .call(calcPosition)
-            .attr({
-                x1: function (d) { return d.get('position').x1; },
-                y1: function (d) { return d.get('position').y1; },
-                x2: function (d) { return d.get('position').x2; },
-                y2: function (d) { return d.get('position').y2; },
-                stroke: linkConfig.light.inColor,
-                'stroke-width': linkConfig.inWidth
-            });
-
-        entering.each(t2d3.linkEntering);
-
-        // operate on exiting links:
-        link.exit()
-            .style('opacity', 1)
-            .transition()
-            .duration(300)
-            .style('opacity', 0.0)
-            .remove();
-    }
-
-    function calcPosition() {
-        var lines = this;
-
-        lines.each(function (d) {
-            if (d.get('type') === 'hostLink') {
-                d.set('position', getDefaultPos(d));
-            }
-        });
-
-        lines.each(function (d) {
-            d.set('position', getDefaultPos(d));
-        });
-    }
-
-    function getDefaultPos(link) {
+    function getDefaultPosition(link) {
         return {
             x1: link.get('source').x,
             y1: link.get('source').y,
@@ -321,156 +86,293 @@
         };
     }
 
-    function setDimensions() {
-        if (force) {
-            force.size(t2vs.getDimensions());
-        }
-    }
-
-    function start() {
-        force.start();
-    }
-
-    function mouseClickHandler() {
-
-        if (!d3.event.shiftKey) {
-            t2rs.deselectLink();
-        }
-
-        if (!t2ss.clickConsumed()) {
-            if (previousNearestLink) {
-                previousNearestLink.select();
-            }
-        }
-
-    }
-
-    // Select Links
-    function mouseMoveHandler() {
-        var mp = getLogicalMousePosition(this),
-            link = computeNearestLink(mp);
-
-        // link.enhance();
-        if (link) {
-            if (previousNearestLink && previousNearestLink != link) {
-                previousNearestLink.unenhance();
-            }
-            link.enhance();
-        } else {
-            if (previousNearestLink) {
-                previousNearestLink.unenhance();
-            }
-        }
-
-        previousNearestLink = link;
-    }
-
-
-    function getLogicalMousePosition(container) {
-        var m = d3.mouse(container),
-            sc = zoomer.scale(),
-            tr = zoomer.translate(),
-            mx = (m[0] - tr[0]) / sc,
-            my = (m[1] - tr[1]) / sc;
-        return {x: mx, y: my};
-    }
-
-    function sq(x) { return x * x; }
-
-    function mdist(p, m) {
-        return Math.sqrt(sq(p.x - m.x) + sq(p.y - m.y));
-    }
-
-    function prox(dist) {
-        return dist / zoomer.scale();
-    }
-
-    function computeNearestLink(mouse) {
-        var proximity = prox(30),
-            nearest = null,
-            minDist;
-
-        function pdrop(line, mouse) {
-            var x1 = line.x1,
-                y1 = line.y1,
-                x2 = line.x2,
-                y2 = line.y2,
-                x3 = mouse.x,
-                y3 = mouse.y,
-                k = ((y2-y1) * (x3-x1) - (x2-x1) * (y3-y1)) /
-                    (sq(y2-y1) + sq(x2-x1)),
-                x4 = x3 - k * (y2-y1),
-                y4 = y3 + k * (x2-x1);
-            return {x:x4, y:y4};
-        }
-
-        function lineHit(line, p, m) {
-            if (p.x < line.x1 && p.x < line.x2) return false;
-            if (p.x > line.x1 && p.x > line.x2) return false;
-            if (p.y < line.y1 && p.y < line.y2) return false;
-            if (p.y > line.y1 && p.y > line.y2) return false;
-            // line intersects, but are we close enough?
-            return mdist(p, m) <= proximity;
-        }
-
-        var links = t2rs.regionLinks();
-
-        if (links.length) {
-            minDist = proximity * 2;
-
-            links.forEach(function (d) {
-                var line = d.get('position'),
-                    point,
-                    hit,
-                    dist;
-
-                // TODO: Reinstate when showHost() is implemented
-                // if (!api.showHosts() && d.type() === 'hostLink') {
-                //     return; // skip hidden host links
-                // }
-
-                if (line) {
-                    point = pdrop(line, mouse);
-                    hit = lineHit(line, point, mouse);
-                    if (hit) {
-                        dist = mdist(point, mouse);
-                        if (dist < minDist) {
-                            minDist = dist;
-                            nearest = d;
-                        }
-                    }
-                }
-            });
-        }
-
-        return nearest;
-    }
-
     angular.module('ovTopo2')
     .factory('Topo2LayoutService',
         [
             '$log', 'WebSocketService', 'SvgUtilService', 'Topo2RegionService',
-            'Topo2D3Service', 'Topo2ViewService', 'Topo2SelectService',
+            'Topo2D3Service', 'Topo2ViewService', 'Topo2SelectService', 'Topo2ZoomService',
+            'Topo2ViewController',
+            function ($log, wss, sus, t2rs, t2d3, t2vs, t2ss, t2zs,
+                      ViewController) {
 
-            function (_$log_, _wss_, _sus_, _t2rs_, _t2d3_, _t2vs_, _t2ss_) {
+                var Layout = ViewController.extend({
+                    initialize: function (svg, forceG, uplink, dim, zoomer, opts) {
 
-                $log = _$log_;
-                wss = _wss_;
-                t2rs = _t2rs_;
-                t2d3 = _t2d3_;
-                t2vs = _t2vs_;
-                t2ss = _t2ss_;
-                sus = _sus_;
+                        $log.debug('initialize Layout');
+                        instance = this;
 
-                return {
-                    init: init,
-                    createForceLayout: createForceLayout,
-                    update: update,
-                    tick: tick,
-                    start: start,
+                        this.svg = svg;
 
-                    setDimensions: setDimensions
-                };
+                        // Append all the SVG Group elements to the forceG object
+                        this.createForceElements();
+
+                        this.uplink = uplink;
+                        this.dim = dim;
+                        this.zoomer = zoomer;
+
+                        this.settings = angular.extend({}, defaultSettings, opts);
+
+                        this.link = this.elements.linkG.selectAll('.link');
+                        this.elements.linkLabelG.selectAll('.linkLabel');
+                        this.node = this.elements.nodeG.selectAll('.node');
+                    },
+                    createForceElements: function () {
+
+                        this.prevForce = this.forceG;
+
+                        this.forceG = d3.select('#topo-zoomlayer')
+                            .append('g').attr('class', 'topo-force');
+
+                        this.elements = {
+                            linkG: this.addElement(this.forceG, 'topo-links'),
+                            linkLabelG: this.addElement(this.forceG, 'topo-linkLabels'),
+                            numLinksLabels: this.addElement(this.forceG, 'topo-numLinkLabels'),
+                            nodeG: this.addElement(this.forceG, 'topo-nodes'),
+                            portLabels: this.addElement(this.forceG, 'topo-portLabels')
+                        };
+                    },
+                    addElement: function (parent, className) {
+                        return parent.append('g').attr('class', className);
+                    },
+                    settingOrDefault: function (settingName, node) {
+                        var nodeType = node.get('nodeType');
+                        return this.settings[settingName][nodeType] || this.settings[settingName]._def_;
+                    },
+                    createForceLayout: function () {
+                        var _this = this,
+                            regionLinks = t2rs.regionLinks(),
+                            regionNodes = t2rs.regionNodes();
+
+                        this.force = d3.layout.force()
+                            .size(t2vs.getDimensions())
+                            .gravity(this.settings.gravity)
+                            .friction(this.settings.friction)
+                            .charge(this.settingOrDefault.bind(this, 'charge'))
+                            .linkDistance(this.settingOrDefault.bind(this, 'linkDistance'))
+                            .linkStrength(this.settingOrDefault.bind(this, 'linkStrength'))
+                            .nodes(regionNodes)
+                            .links(regionLinks)
+                            .on("tick", this.tick.bind(this))
+                            .on("start", function () {
+
+                                // TODO: Find a better way to do this
+                                setTimeout(function () {
+                                    _this.centerLayout();
+                                }, 500);
+                            })
+                            .start();
+
+                        this.link = this.elements.linkG.selectAll('.link')
+                            .data(regionLinks, function (d) { return d.get('key'); });
+
+                        this.node = this.elements.nodeG.selectAll('.node')
+                            .data(regionNodes, function (d) { return d.get('id'); });
+
+                        this.drag = sus.createDragBehavior(this.force,
+                            t2ss.selectObject,
+                            this.atDragEnd,
+                            this.dragEnabled.bind(this),
+                            clickEnabled
+                        );
+
+                        this.update();
+                    },
+                    centerLayout: function () {
+                        d3.select('#topo-zoomlayer').attr('data-layout', t2rs.model.get('id'));
+
+                        var zoomer = d3.select('#topo-zoomlayer').node().getBBox(),
+                            layoutBBox = this.forceG.node().getBBox(),
+                            scale = (zoomer.height - 150) / layoutBBox.height,
+                            x = (zoomer.width / 2) - ((layoutBBox.x + layoutBBox.width / 2) * scale),
+                            y = (zoomer.height / 2) - ((layoutBBox.y + layoutBBox.height / 2) * scale);
+
+                        t2zs.panAndZoom([x, y], scale, 1000);
+                    },
+                    tick: function () {
+                        this.link
+                            .attr("x1", function (d) { return d.source.x; })
+                            .attr("y1", function (d) { return d.source.y; })
+                            .attr("x2", function (d) { return d.target.x; })
+                            .attr("y2", function (d) { return d.target.y; });
+
+                        this.node
+                            .attr({
+                                transform: function (d) {
+                                    var dx = isNaN(d.x) ? 0 : d.x,
+                                        dy = isNaN(d.y) ? 0 : d.y;
+                                    return sus.translate(dx, dy);
+                                }
+                            });
+                    },
+
+                    start: function () {
+                        this.force.start();
+                    },
+                    update: function () {
+                        this.updateNodes();
+                        this.updateLinks();
+                    },
+                    updateNodes: function () {
+                        var regionNodes = t2rs.regionNodes();
+
+                        // select all the nodes in the layout:
+                        this.node = this.elements.nodeG.selectAll('.node')
+                            .data(regionNodes, function (d) { return d.get('id'); });
+
+                        var entering = this.node.enter()
+                            .append('g')
+                            .attr({
+                                id: function (d) { return sus.safeId(d.get('id')); },
+                                class: function (d) { return d.svgClassName(); },
+                                transform: function (d) {
+                                    // Need to guard against NaN here ??
+                                    return sus.translate(d.node.x, d.node.y);
+                                },
+                                opacity: 0
+                            })
+                            .call(this.drag)
+                            .transition()
+                            .attr('opacity', 1);
+
+                        entering.filter('.device').each(t2d3.nodeEnter);
+                        entering.filter('.sub-region').each(t2d3.nodeEnter);
+                        entering.filter('.host').each(t2d3.hostEnter);
+
+                        // operate on exiting nodes:
+                        // Note that the node is removed after 2 seconds.
+                        // Sub element animations should be shorter than 2 seconds.
+                        // var exiting = this.node.exit()
+                        //     .transition()
+                        //     .duration(300)
+                        //     .style('opacity', 0)
+                        //     .remove();
+
+                        // exiting node specifics:
+                        // exiting.filter('.host').each(t2d3.hostExit);
+                        // exiting.filter('.device').each(t2d3.nodeExit);
+                    },
+                    updateLinks: function () {
+
+                        var regionLinks = t2rs.regionLinks();
+
+                        this.link = this.elements.linkG.selectAll('.link')
+                            .data(regionLinks, function (d) { return d.get('key'); });
+
+                        // operate on entering links:
+                        var entering = this.link.enter()
+                            .append('line')
+                            .call(this.calcPosition)
+                            .attr({
+                                x1: function (d) { return d.get('position').x1; },
+                                y1: function (d) { return d.get('position').y1; },
+                                x2: function (d) { return d.get('position').x2; },
+                                y2: function (d) { return d.get('position').y2; },
+                                stroke: linkConfig.light.inColor,
+                                'stroke-width': linkConfig.inWidth
+                            });
+
+                        entering.each(t2d3.linkEntering);
+
+                        // operate on exiting links:
+                        this.link.exit()
+                            .style('opacity', 1)
+                            .transition()
+                            .duration(300)
+                            .style('opacity', 0.0)
+                            .remove();
+                    },
+                    calcPosition: function () {
+                        var lines = this;
+
+                        lines.each(function (d) {
+                            if (d.get('type') === 'hostLink') {
+                                d.set('position', getDefaultPosition(d));
+                            }
+                        });
+
+                        lines.each(function (d) {
+                            d.set('position', getDefaultPosition(d));
+                        });
+                    },
+                    sendUpdateMeta: function (d, clearPos) {
+                        var metaUi = {},
+                            ll;
+
+                        // if we are not clearing the position data (unpinning),
+                        // attach the x, y, (and equivalent longitude, latitude)...
+                        if (!clearPos) {
+                            ll = d.lngLatFromCoord([d.x, d.y]);
+                            metaUi = {
+                                x: d.x,
+                                y: d.y,
+                                equivLoc: {
+                                    lng: ll[0],
+                                    lat: ll[1]
+                                }
+                            };
+                        }
+                        d.metaUi = metaUi;
+                        wss.sendEvent('updateMeta2', {
+                            id: d.get('id'),
+                            class: d.get('class'),
+                            memento: metaUi
+                        });
+                    },
+                    setDimensions: function () {
+                        if (this.force) {
+                            this.force.size(t2vs.getDimensions());
+                        }
+                    },
+                    dragEnabled: function () {
+                        var ev = d3.event.sourceEvent;
+                        // nodeLock means we aren't allowing nodes to be dragged...
+                        return !nodeLock && !this.zoomingOrPanning(ev);
+                    },
+                    zoomingOrPanning: function (ev) {
+                        return ev.metaKey || ev.altKey;
+                    },
+                    atDragEnd: function (d) {
+                        // once we've finished moving, pin the node in position
+                        d.fixed = true;
+                        d3.select(this).classed('fixed', true);
+                        instance.sendUpdateMeta(d);
+                        $log.debug(d);
+                        t2ss.clickConsumed(true);
+                    },
+                    transitionDownRegion: function () {
+
+                        this.prevForce.transition()
+                            .duration(1500)
+                            .style('opacity', 0)
+                            .remove();
+
+                        this.forceG
+                            .style('opacity', 0)
+                            .transition()
+                            .delay(500)
+                            .duration(500)
+                            .style('opacity', 1);
+                    },
+                    transitionUpRegion: function () {
+                        this.prevForce.transition()
+                            .duration(1000)
+                            .style('opacity', 0)
+                            .remove();
+
+                        this.forceG
+                            .style('opacity', 0)
+                            .transition()
+                            .delay(500)
+                            .duration(500)
+                            .style('opacity', 1);
+                    }
+                });
+
+                function getInstance(svg, forceG, uplink, dim, zoomer, opts) {
+                    return instance || new Layout(svg, forceG, uplink, dim, zoomer, opts);
+                }
+
+                return getInstance;
             }
         ]
     );
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 d769631..bcd315d 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -94,9 +94,10 @@
 
     function linkEndPoints(srcId, dstId) {
 
-        var allNodes = this.region.nodes();
-        var sourceNode = this.region.findNodeById(this, srcId);
-        var targetNode = this.region.findNodeById(this, dstId);
+        var findNodeById = this.region.model.findNodeById.bind(this.region),
+            allNodes = this.region.model.nodes(),
+            sourceNode = findNodeById(this, srcId),
+            targetNode = findNodeById(this, dstId);
 
         if (!sourceNode || !targetNode) {
             $log.error('Node(s) not on map for link:' + srcId + '~' + dstId);
@@ -157,13 +158,8 @@
                 var data = [],
                     point;
 
-                // angular.forEach(this.collection.models, function (link) {
-                //     link.unenhance();
-                // });
-
-                this.set('enhanced', true);
-
                 if (showPort()) {
+                    this.set('enhanced', true);
                     point = this.locatePortLabel();
                     angular.extend(point, {
                         id: 'topo-port-tgt',
@@ -185,7 +181,7 @@
                         .data(data)
                         .enter().append('g')
                         .classed('portLabel', true)
-                        .attr('id', function (d) { return d.id; })
+                        .attr('id', function (d) { return d.id; });
 
                     entering.each(function (d) {
                         var el = d3.select(this),
@@ -219,8 +215,6 @@
             },
             select: function () {
 
-                var ev = d3.event;
-
                 // TODO: if single selection clear selected devices, hosts, sub-regions
                 var s = Boolean(this.get('selected'));
                 // Clear all selected Items
@@ -340,27 +334,26 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2LinkService',
-        ['$log', 'Topo2Collection', 'Topo2Model',
+    .factory('Topo2LinkService', [
+        '$log', 'Topo2Collection', 'Topo2Model',
         'ThemeService', 'SvgUtilService', 'Topo2ZoomService',
         'Topo2ViewService', 'Topo2LinkPanelService', 'FnService',
-            function (_$log_, _Collection_, _Model_, _ts_, _sus_,
-                _t2zs_, _t2vs_, _t2lps_, _fn_) {
+        function (_$log_, _c_, _Model_, _ts_, _sus_,
+            _t2zs_, _t2vs_, _t2lps_, _fn_) {
 
-                $log = _$log_;
-                ts = _ts_;
-                sus = _sus_;
-                t2zs = _t2zs_;
-                t2vs = _t2vs_;
-                Collection = _Collection_;
-                Model = _Model_;
-                t2lps = _t2lps_;
-                fn = _fn_;
+            $log = _$log_;
+            ts = _ts_;
+            sus = _sus_;
+            t2zs = _t2zs_;
+            t2vs = _t2vs_;
+            Collection = _c_;
+            Model = _Model_;
+            t2lps = _t2lps_;
+            fn = _fn_;
 
-                return {
-                    createLinkCollection: createLinkCollection
-                };
-            }
-        ]);
-
+            return {
+                createLinkCollection: createLinkCollection
+            };
+        }
+    ]);
 })();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2LinkPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2LinkPanel.js
index cb65ced..ea096b7 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2LinkPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2LinkPanel.js
@@ -23,13 +23,13 @@
     'use strict';
 
     // Injected Services
-    var Panel, gs, wss, flash, listProps;
+    var panel, gs, flash, ls;
 
     // Internal State
     var linkPanel, linkData;
 
     function init() {
-        linkPanel = Panel();
+        linkPanel = panel();
     }
 
     function formatLinkData(data) {
@@ -49,15 +49,15 @@
                 'Type': data.get('type'),
                 'A Type': source.get('nodeType'),
                 'A Id': source.get('id'),
-                'A Label': 'Label',
-                'A Port': data.get('portA') || '',
+                'A Label': source.get('props').name,
+                'A Port': data.get('portA') || 'N/A',
                 'B Type': target.get('nodeType'),
                 'B Id': target.get('id'),
-                'B Label': 'Label',
-                'B Port': data.get('portB') || '',
+                'B Label': target.get('props').name,
+                'B Port': data.get('portB') || 'N/A'
             }
-        }
-    };
+        };
+    }
 
     function displayLink(data) {
         init();
@@ -79,7 +79,7 @@
 
         title.text(linkData.title);
         gs.addGlyph(svg, 'bird', 24, 0, [1, 1]);
-        listProps(tbody, linkData);
+        ls.listProps(tbody, linkData);
     }
 
     function show() {
@@ -97,20 +97,18 @@
     }
 
     function destroy() {
-        wss.unbindHandlers(handlerMap);
         linkPanel.destroy();
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2LinkPanelService',
-    ['Topo2DetailsPanelService', 'GlyphService', 'WebSocketService', 'FlashService', 'ListService',
-        function (_ps_, _gs_, _wss_, _flash_, _listService_) {
+    .factory('Topo2LinkPanelService', [
+        'Topo2DetailsPanelService', 'GlyphService', 'FlashService', 'ListService',
+        function (_ps_, _gs_, _flash_, _ls_) {
 
-            Panel = _ps_;
+            panel = _ps_;
             gs = _gs_;
-            wss = _wss_;
             flash = _flash_;
-            listProps = _listService_;
+            ls = _ls_;
 
             return {
                 displayLink: displayLink,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Model.js b/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
index f76456e..c8430fb 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
@@ -22,8 +22,6 @@
 (function () {
     'use strict';
 
-    var extend;
-
     function Model(attributes) {
 
         var attrs = attributes || {};
@@ -125,7 +123,7 @@
         'FnService',
         function (fn) {
             Model.extend = fn.extend;
-            
+
             return Model;
         }
     ]);
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 cf5bb15..7171477 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
@@ -53,8 +53,8 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2NodeModel',
-        ['Topo2Model', 'FnService', 'Topo2PrefsService',
+    .factory('Topo2NodeModel', [
+        'Topo2Model', 'FnService', 'Topo2PrefsService',
         'SvgUtilService', 'IconService', 'ThemeService',
         'Topo2MapConfigService', 'Topo2ZoomService', 'Topo2NodePositionService',
         function (Model, _fn_, _ps_, _sus_, _is_, _ts_,
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 686ec96..88ba678 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Panel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Panel.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // Injected Services
-    var flash, ps;
+    var ps;
 
     var panel = {
         initialize: function (id, options) {
@@ -61,14 +61,13 @@
         isVisible: function () {
             return this.el.isVisible();
         }
-    }
+    };
 
     angular.module('ovTopo2')
-    .factory('Topo2PanelService',
-    ['Topo2UIView', 'FlashService', 'PanelService',
-        function (View, _flash_, _ps_) {
+    .factory('Topo2PanelService', [
+        'Topo2UIView', 'PanelService',
+        function (View, _ps_) {
 
-            flash = _flash_;
             ps = _ps_;
 
             return View.extend(panel);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Prefs.js b/web/gui/src/main/webapp/app/view/topo2/topo2Prefs.js
index 9eaef00..690ac59 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Prefs.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Prefs.js
@@ -50,9 +50,8 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2PrefsService',
-    ['PrefsService',
-
+    .factory('Topo2PrefsService', [
+        'PrefsService',
         function (_ps_) {
 
             ps = _ps_;
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 b40eff7..09b8cd8 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -23,189 +23,129 @@
     'use strict';
 
     // Injected Services
-    var $log, t2sr, t2ds, t2hs, t2ls, t2zs, t2dps, t2bcs;
     var Model;
 
     // Internal
-    var region
+    var instance
 
     // 'static' vars
     var ROOT = '(root)';
 
-    function init() {}
-
-    function addRegion(data) {
-
-        var RegionModel = Model.extend({
-            findNodeById: findNodeById,
-            nodes: regionNodes
-        });
-
-        region = new RegionModel({
-            id: data.id,
-            layerOrder: data.layerOrder
-        });
-
-        region.set({
-            subregions: t2sr.createSubRegionCollection(data.subregions, region),
-            devices: t2ds.createDeviceCollection(data.devices, region),
-            hosts: t2hs.createHostCollection(data.hosts, region),
-            links: t2ls.createLinkCollection(data.links, region)
-        });
-
-        angular.forEach(region.get('links').models, function (link) {
-            link.createLink();
-        });
-
-        // TODO: replace with an algorithm that computes appropriate transition
-        //        based on the location of the "region node" on the parent map
-
-        // TEMP Map Zoom
-        var regionPanZooms = {
-            "(root)": {
-                scale: 4.21,
-                translate: [-2066.3049871603093, -2130.190726668792]
-            },
-            c01: {
-                scale: 19.8855,
-                translate: [-10375.91165337411, -10862.217941271818]
-            },
-            c02: {
-                scale: 24.25,
-                translate: [-14169.70851936781, -15649.174761455488]
-            },
-            c03: {
-                scale: 22.72,
-                translate: [-14950.92246589002, -15390.955326616648]
-            },
-            c04: {
-                scale: 26.24,
-                translate: [-16664.006814209282, -16217.021478816077]
-            }
-        };
-
-
-        // Hide Breadcrumbs if there are no subregions configured in the root region
-        if (isRootRegion() && !region.get('subregions').models.length) {
-            t2bcs.hide();
-        }
-
-        setTimeout(function () {
-            var regionPZ = regionPanZooms[region.get('id')];
-            t2zs.panAndZoom(regionPZ.translate, regionPZ.scale);
-        }, 10);
-
-        $log.debug('Region: ', region);
-    }
-
-    function findNodeById(link, id) {
-
-
-        if (link.get('type') !== 'UiEdgeLink') {
-            // Remove /{port} from id if needed
-            var regex = new RegExp('^[^/]*');
-            id = regex.exec(id)[0];
-        }
-
-        return region.get('devices').get(id) ||
-            region.get('hosts').get(id) ||
-            region.get('subregions').get(id);
-    }
-
-    function regionNodes() {
-
-        if (region) {
-            return [].concat(
-                region.get('devices').models,
-                region.get('hosts').models,
-                region.get('subregions').models
-            );
-        }
-
-        return [];
-    }
-
-    function filterRegionNodes(predicate) {
-        var nodes = regionNodes();
-        return _.filter(nodes, predicate);
-    }
-
-    function regionLinks() {
-        return (region) ? region.get('links').models : [];
-    }
-
-    function deselectAllNodes() {
-
-        var selected = filterRegionNodes(function (node) {
-            return node.get('selected', true);
-        });
-
-        if (selected.length) {
-
-            selected.forEach(function (node) {
-                node.deselect();
-            });
-
-            t2dps().el.hide();
-            return true;
-        }
-
-        return false;
-    }
-
-    function deselectLink() {
-
-        var selected = _.filter(regionLinks(), function (link) {
-            return link.get('selected', true);
-        });
-
-        if (selected.length) {
-
-            selected.forEach(function (link) {
-                link.deselect();
-            });
-
-            t2dps().el.hide();
-            return true;
-        }
-
-        return false;
-    }
-
-    function isRootRegion() {
-        return region.get('id') === ROOT;
-    }
-
     angular.module('ovTopo2')
-    .factory('Topo2RegionService',
-        ['$log', 'Topo2Model',
-        'Topo2SubRegionService', 'Topo2DeviceService',
+    .factory('Topo2RegionService', [
+        '$log', 'Topo2Model', 'Topo2SubRegionService', 'Topo2DeviceService',
         'Topo2HostService', 'Topo2LinkService', 'Topo2ZoomService', 'Topo2DetailsPanelService',
-        'Topo2BreadcrumbService',
+        'Topo2BreadcrumbService', 'Topo2ViewController',
+        function ($log, _Model_, t2sr, t2ds, t2hs, t2ls, t2zs, t2dps, t2bcs, ViewController) {
 
-        function (_$log_, _Model_, _t2sr_, _t2ds_, _t2hs_, _t2ls_, _t2zs_, _t2dps_, _t2bcs_) {
-
-            $log = _$log_;
             Model = _Model_;
-            t2sr = _t2sr_;
-            t2ds = _t2ds_;
-            t2hs = _t2hs_;
-            t2ls = _t2ls_;
-            t2zs = _t2zs_;
-            t2dps = _t2dps_;
-            t2bcs = _t2bcs_;
 
-            return {
-                init: init,
+            var Region = ViewController.extend({
+                initialize: function () {
+                    instance = this;
+                    this.model = null;
+                },
+                addRegion: function (data) {
 
-                addRegion: addRegion,
-                regionNodes: regionNodes,
-                regionLinks: regionLinks,
-                filterRegionNodes: filterRegionNodes,
+                    var RegionModel = Model.extend({
+                        findNodeById: this.findNodeById,
+                        nodes: this.regionNodes.bind(this)
+                    });
 
-                deselectAllNodes: deselectAllNodes,
-                deselectLink: deselectLink,
-            };
+                    this.model = new RegionModel({
+                        id: data.id,
+                        layerOrder: data.layerOrder
+                    });
+
+                    this.model.set({
+                        subregions: t2sr.createSubRegionCollection(data.subregions, this),
+                        devices: t2ds.createDeviceCollection(data.devices, this),
+                        hosts: t2hs.createHostCollection(data.hosts, this),
+                        links: t2ls.createLinkCollection(data.links, this)
+                    });
+
+                    angular.forEach(this.model.get('links').models, function (link) {
+                        link.createLink();
+                    });
+
+                    // Hide Breadcrumbs if there are no subregions configured in the root region
+                    if (this.isRootRegion() && !this.model.get('subregions').models.length) {
+                        t2bcs.hide();
+                    }
+                },
+                isRootRegion: function () {
+                    return this.model.get('id') === ROOT;
+                },
+                findNodeById: function (link, id) {
+                    if (link.get('type') !== 'UiEdgeLink') {
+                        // Remove /{port} from id if needed
+                        var regex = new RegExp('^[^/]*');
+                        id = regex.exec(id)[0];
+                    }
+                    return this.model.get('devices').get(id) ||
+                        this.model.get('hosts').get(id) ||
+                        this.model.get('subregions').get(id);
+                },
+                regionNodes: function () {
+
+                    if (this.model) {
+                        return [].concat(
+                            this.model.get('devices').models,
+                            this.model.get('hosts').models,
+                            this.model.get('subregions').models
+                        );
+                    }
+
+                    return [];
+                },
+                regionLinks: function () {
+                    return (this.model) ? this.model.get('links').models : [];
+                },
+                filterRegionNodes: function (predicate) {
+                    var nodes = this.regionNodes();
+                    return _.filter(nodes, predicate);
+                },
+                deselectAllNodes: function () {
+                    var selected = this.filterRegionNodes(function (node) {
+                        return node.get('selected', true);
+                    });
+
+                    if (selected.length) {
+
+                        selected.forEach(function (node) {
+                            node.deselect();
+                        });
+
+                        t2dps().el.hide();
+                        return true;
+                    }
+
+                    return false;
+                },
+                deselectLink: function () {
+                    var selected = _.filter(this.regionLinks(), function (link) {
+                        return link.get('selected', true);
+                    });
+
+                    if (selected.length) {
+
+                        selected.forEach(function (link) {
+                            link.deselect();
+                        });
+
+                        t2dps().el.hide();
+                        return true;
+                    }
+
+                    return false;
+                }
+            });
+
+            function getInstance() {
+                return instance || new Region();
+            }
+
+            return getInstance();
         }]);
 
 })();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Select.js b/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
index 073147d..c9972a9 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
@@ -21,8 +21,18 @@
 (function () {
     'use strict';
 
+    var t2rs;
+
     // internal state
-    var consumeClick;
+    var consumeClick,
+        zoomer,
+        previousNearestLink;    // previous link to mouse position
+
+    function init(svg, _zoomer_) {
+        zoomer = _zoomer_;
+        svg.on('mousemove', mouseMoveHandler);
+        svg.on('click', mouseClickHandler);
+    }
 
     function selectObject(obj) {}
 
@@ -32,11 +42,129 @@
         return cc;
     }
 
+    function mouseClickHandler() {
+
+        if (!d3.event.shiftKey) {
+            t2rs.deselectLink();
+        }
+
+        if (!clickConsumed()) {
+            if (previousNearestLink) {
+                previousNearestLink.select();
+            }
+        }
+
+    }
+
+    // Select Links
+    function mouseMoveHandler() {
+        var mp = getLogicalMousePosition(this),
+            link = computeNearestLink(mp);
+
+        // link.enhance();
+        if (link) {
+            if (previousNearestLink && previousNearestLink !== link) {
+                previousNearestLink.unenhance();
+            }
+            link.enhance();
+        } else if (previousNearestLink) {
+            previousNearestLink.unenhance();
+        }
+
+        previousNearestLink = link;
+    }
+
+    function getLogicalMousePosition(container) {
+        var m = d3.mouse(container),
+            sc = zoomer.scale(),
+            tr = zoomer.translate(),
+            mx = (m[0] - tr[0]) / sc,
+            my = (m[1] - tr[1]) / sc;
+        return { x: mx, y: my };
+    }
+
+    function sq(x) {
+        return x * x;
+    }
+
+    function mdist(p, m) {
+        return Math.sqrt(sq(p.x - m.x) + sq(p.y - m.y));
+    }
+
+    function prox(dist) {
+        return dist / zoomer.scale();
+    }
+
+    function computeNearestLink(mouse) {
+        var proximity = prox(30),
+            nearest = null,
+            minDist;
+
+        function pdrop(line, mouse) {
+            var x1 = line.x1,
+                y1 = line.y1,
+                x2 = line.x2,
+                y2 = line.y2,
+                x3 = mouse.x,
+                y3 = mouse.y,
+                k = ((y2 - y1) * (x3 - x1) - (x2 - x1) * (y3 - y1)) /
+                    (sq(y2 - y1) + sq(x2 - x1)),
+                x4 = x3 - k * (y2 - y1),
+                y4 = y3 + k * (x2 - x1);
+            return { x: x4, y: y4 };
+        }
+
+        function lineHit(line, p, m) {
+            if (p.x < line.x1 && p.x < line.x2) return false;
+            if (p.x > line.x1 && p.x > line.x2) return false;
+            if (p.y < line.y1 && p.y < line.y2) return false;
+            if (p.y > line.y1 && p.y > line.y2) return false;
+            // line intersects, but are we close enough?
+            return mdist(p, m) <= proximity;
+        }
+
+        var links = t2rs.regionLinks();
+
+        if (links.length) {
+            minDist = proximity * 2;
+
+            links.forEach(function (d) {
+                var line = d.get('position'),
+                    point,
+                    hit,
+                    dist;
+
+                // TODO: Reinstate when showHost() is implemented
+                // if (!api.showHosts() && d.type() === 'hostLink') {
+                //     return; // skip hidden host links
+                // }
+
+                if (line) {
+                    point = pdrop(line, mouse);
+                    hit = lineHit(line, point, mouse);
+                    if (hit) {
+                        dist = mdist(point, mouse);
+                        if (dist < minDist) {
+                            minDist = dist;
+                            nearest = d;
+                        }
+                    }
+                }
+            });
+        }
+
+        return nearest;
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2SelectService', [
-        function () {
+        'Topo2RegionService',
+        function (_t2rs_) {
+
+            t2rs = _t2rs_;
 
             return {
+                init: init,
                 selectObject: selectObject,
                 clickConsumed: clickConsumed
             };
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
index 75a2ef4..0852788 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
@@ -22,7 +22,6 @@
 (function () {
     'use strict';
 
-    var wss;
     var Collection, Model;
 
     var remappedDeviceTypes = {
@@ -32,68 +31,72 @@
     function createSubRegionCollection(data, region) {
 
         var SubRegionCollection = Collection.extend({
-            model: Model
+            model: Model,
+            region: region
         });
 
         return new SubRegionCollection(data);
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2SubRegionService',
-        ['WebSocketService', 'Topo2Collection', 'Topo2NodeModel',
-        'ThemeService', 'Topo2ViewService', 'Topo2SubRegionPanelService',
+    .factory('Topo2SubRegionService', [
+        'WebSocketService', 'Topo2Collection', 'Topo2NodeModel',
+        'Topo2SubRegionPanelService',
 
-            function (_wss_, _c_, _NodeModel_, _ts_, _t2vs_m, _t2srp_) {
+        function (wss, _c_, NodeModel, t2srp) {
 
-                wss = _wss_;
-                Collection = _c_;
+            Collection = _c_;
 
-                Model = _NodeModel_.extend({
-                    initialize: function () {
-                        this.super = this.constructor.__super__;
-                        this.super.initialize.apply(this, arguments);
-                    },
-                    events: {
-                        'dblclick': 'navigateToRegion',
-                        'click': 'onClick'
-                    },
-                    onChange: function () {
-                        // Update class names when the model changes
-                        if (this.el) {
-                            this.el.attr('class', this.svgClassName());
-                        }
-                    },
-                    nodeType: 'sub-region',
-                    icon: function () {
-                        var type = this.get('type');
-                        return remappedDeviceTypes[type] || type || 'm_cloud';
-                    },
-                    onClick: function () {
-                        var selected = this.select(d3.event);
-
-                        if (selected.length > 0) {
-                            _t2srp_.displayPanel(this);
-                        } else {
-                            _t2srp_.hide();
-                        }
-                    },
-                    navigateToRegion: function () {
-
-                        if (d3.event.defaultPrevented) return;
-
-                        wss.sendEvent('topo2navRegion', {
-                            dir: 'down',
-                            rid: this.get('id')
-                        });
-
-                        _t2srp_.hide();
+            Model = NodeModel.extend({
+                initialize: function () {
+                    this.super = this.constructor.__super__;
+                    this.super.initialize.apply(this, arguments);
+                },
+                events: {
+                    'dblclick': 'navigateToRegion',
+                    'click': 'onClick'
+                },
+                onChange: function () {
+                    // Update class names when the model changes
+                    if (this.el) {
+                        this.el.attr('class', this.svgClassName());
                     }
-                });
+                },
+                nodeType: 'sub-region',
+                icon: function () {
+                    var type = this.get('type');
+                    return remappedDeviceTypes[type] || type || 'm_cloud';
+                },
+                onClick: function () {
+                    var selected = this.select(d3.event);
 
-                return {
-                    createSubRegionCollection: createSubRegionCollection
-                };
-            }
-        ]);
+                    if (selected.length > 0) {
+                        t2srp.displayPanel(this);
+                    } else {
+                        t2srp.hide();
+                    }
+                },
+                navigateToRegion: function () {
+
+                    if (d3.event.defaultPrevented) return;
+
+                    wss.sendEvent('topo2navRegion', {
+                        dir: 'down',
+                        rid: this.get('id')
+                    });
+
+                    var layout = this.collection.region.layout;
+                    layout.createForceElements();
+                    layout.transitionDownRegion();
+
+                    t2srp.hide();
+                }
+            });
+
+            return {
+                createSubRegionCollection: createSubRegionCollection
+            };
+        }
+    ]);
 
 })();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegionPanel.js b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegionPanel.js
index b5e49b7..0185e89 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegionPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegionPanel.js
@@ -23,13 +23,13 @@
     'use strict';
 
     // Injected Services
-    var Panel, gs, wss, flash, listProps;
+    var panel, gs, flash, ls;
 
     // Internal State
     var subRegionPanel, subRegionData;
 
     function init() {
-        subRegionPanel = Panel();
+        subRegionPanel = panel();
     }
 
     function formatSubRegionData(data) {
@@ -43,8 +43,8 @@
                 'Number of Devices': data.get('nDevs'),
                 'Number of Hosts': data.get('nHosts')
             }
-        }
-    };
+        };
+    }
 
     function displayPanel(data) {
         init();
@@ -65,7 +65,7 @@
 
         title.text(subRegionData.title);
         gs.addGlyph(svg, 'bird', 24, 0, [1, 1]);
-        listProps(tbody, subRegionData);
+        ls.listProps(tbody, subRegionData);
     }
 
     function show() {
@@ -87,15 +87,14 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2SubRegionPanelService',
-    ['Topo2DetailsPanelService', 'GlyphService', 'WebSocketService', 'FlashService', 'ListService',
-        function (_ps_, _gs_, _wss_, _flash_, _listService_) {
+    .factory('Topo2SubRegionPanelService', [
+        'Topo2DetailsPanelService', 'GlyphService', 'FlashService', 'ListService',
+        function (_ps_, _gs_, _flash_, _ls_) {
 
-            Panel = _ps_;
+            panel = _ps_;
             gs = _gs_;
-            wss = _wss_;
             flash = _flash_;
-            listProps = _listService_;
+            ls = _ls_;
 
             return {
                 displayPanel: displayPanel,
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 682daac..6017504 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2SummaryPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SummaryPanel.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // Injected Services
-    var Panel, gs, wss, flash, listProps;
+    var Panel, gs, wss, flash, ls;
 
     // Internal State
     var summaryPanel, summaryData;
@@ -64,7 +64,7 @@
 
         title.text(summaryData.title);
         gs.addGlyph(svg, 'bird', 24, 0, [1, 1]);
-        listProps(tbody, summaryData);
+        ls.listProps(tbody, summaryData);
     }
 
     function handleSummaryData(data) {
@@ -91,15 +91,15 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2SummaryPanelService',
-    ['Topo2PanelService', 'GlyphService', 'WebSocketService', 'FlashService', 'ListService',
-        function (_ps_, _gs_, _wss_, _flash_, _listService_) {
+    .factory('Topo2SummaryPanelService', [
+        'Topo2PanelService', 'GlyphService', 'WebSocketService', 'FlashService', 'ListService',
+        function (_ps_, _gs_, _wss_, _flash_, _ls_) {
 
             Panel = _ps_;
             gs = _gs_;
             wss = _wss_;
             flash = _flash_;
-            listProps = _listService_;
+            ls = _ls_;
 
             return {
                 init: init,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2ViewController.js b/web/gui/src/main/webapp/app/view/topo2/topo2ViewController.js
new file mode 100644
index 0000000..9d9f399
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2ViewController.js
@@ -0,0 +1,43 @@
+/*
+ * 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 -- View Controller.
+ A base class for view controllers to extend from
+ */
+
+(function () {
+    'use strict';
+
+    function ViewController(options) {
+        this.initialize.apply(this, arguments);
+    }
+
+    ViewController.prototype = {
+        initialize: function () {
+
+        }
+    };
+
+    angular.module('ovTopo2')
+        .factory('Topo2ViewController', [
+            'FnService',
+            function (fn) {
+                ViewController.extend = fn.extend;
+                return ViewController;
+            }
+        ]);
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Zoom.js b/web/gui/src/main/webapp/app/view/topo2/topo2Zoom.js
index 89e7428..7dcf325 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Zoom.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Zoom.js
@@ -28,6 +28,10 @@
     var zoomer,
         zoomEventListeners = [];
 
+    function getZoomer() {
+        return zoomer;
+    }
+
     function createZoomer(options) {
         var settings = angular.extend({}, options, {
             zoomCallback: zoomCallback
@@ -78,14 +82,15 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2ZoomService',
-        ['ZoomService', 'PrefsService',
+    .factory('Topo2ZoomService', [
+        'ZoomService', 'PrefsService',
         function (_zs_, _ps_) {
 
             zs = _zs_;
             ps = _ps_;
 
             return {
+                getZoomer: getZoomer,
                 createZoomer: createZoomer,
                 addZoomEventListener: addZoomEventListener,
                 removeZoomEventListener: removeZoomEventListener,
diff --git a/web/gui/src/main/webapp/app/view/topo2/uiView.js b/web/gui/src/main/webapp/app/view/topo2/uiView.js
index 8bc01cc..77419a5 100644
--- a/web/gui/src/main/webapp/app/view/topo2/uiView.js
+++ b/web/gui/src/main/webapp/app/view/topo2/uiView.js
@@ -32,8 +32,8 @@
     }
 
     angular.module('ovTopo2')
-    .factory('Topo2UIView',
-    ['FnService',
+    .factory('Topo2UIView', [
+        'FnService',
         function (fn) {
 
             _.extend(View.prototype, {