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/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;
             }
         ]
     );