Grouped injected vars and shortened findNodeById
Fixed syntax in Topo2NodeModel.js
Added SubRegions to the topo2 view

Change-Id: I04c793f3a9a98808eaa11049f31bbb166cc5b66f
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css b/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
index 3bdb8b2..e1551e4 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
@@ -171,6 +171,15 @@
     fill: #454545;
 }
 
+#ov-topo2 svg .node.sub-region rect {
+    fill: #ffffff;
+}
+
+#ov-topo2 svg .node.sub-region use {
+    /* NOTE: this gets overridden programatically */
+    fill: #454545;
+}
+
 
 #ov-topo2 svg .node.device.selected rect {
     stroke-width: 2.0;
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 604c907..2cd4ba7 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
@@ -111,8 +111,8 @@
         }
     }
 
-    function deviceEnter(device) {
-        device.onEnter(this, device);
+    function nodeEnter(node) {
+        node.onEnter(this, node);
     }
 
     function hostLabel(d) {
@@ -153,7 +153,7 @@
 
             return {
                 init: init,
-                deviceEnter: deviceEnter,
+                nodeEnter: nodeEnter,
                 hostEnter: hostEnter,
                 linkEntering: linkEntering
             }
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
index 5a4b528..c8478cf 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
@@ -30,7 +30,6 @@
 
     // configuration
     var devIconDim = 36,
-        labelPad = 10,
         hostRadius = 14,
         badgeConfig = {
             radius: 12,
@@ -44,8 +43,7 @@
             i: 'badgeInfo',
             w: 'badgeWarn',
             e: 'badgeError'
-        },
-        deviceLabelIndex = 0;
+        };
 
     function createDeviceCollection(data, region) {
 
@@ -74,24 +72,6 @@
         return remappedDeviceTypes[type] || type || 'unknown';
     }
 
-    function deviceLabel(d) {
-        //TODO: Device Json is missing labels array
-        return "";
-        var labels = this.get('labels'),
-            idx = (deviceLabelIndex < labels.length) ? deviceLabelIndex : 0;
-        return labels[idx];
-    }
-
-    function trimLabel(label) {
-        return (label && label.trim()) || '';
-    }
-
-    function computeLabelWidth() {
-        var text = this.select('text'),
-        box = text.node().getBBox();
-        return box.width + labelPad * 2;
-    }
-
     function iconBox(dim, labelWidth) {
         return {
             x: -dim / 2,
@@ -104,7 +84,7 @@
     function deviceGlyphColor(d) {
 
         var o = this.node.online,
-            id = "127.0.0.1", // TODO: This should be from node.master
+            id = this.node.master, // TODO: This should be from node.master
             otag = o ? 'online' : 'offline';
         return o ? sus.cat7().getColor(id, 0, ts.theme())
                  : '#ff0000';
@@ -134,32 +114,25 @@
                         this.constructor.__super__.initialize.apply(this, arguments);
                     },
                     nodeType: 'device',
-                    deviceLabel: deviceLabel,
                     deviceGlyphColor: deviceGlyphColor,
                     mapDeviceTypeToGlyph: mapDeviceTypeToGlyph,
-                    trimLabel: trimLabel,
                     setDeviceColor: setDeviceColor,
                     onEnter: function (el) {
 
                         var node = d3.select(el),
                             glyphId = mapDeviceTypeToGlyph(this.get('type')),
-                            label = trimLabel(this.deviceLabel()),
-                            rect, text, glyph, labelWidth;
+                            label = this.trimLabel(this.label()),
+                            glyph, labelWidth;
 
                         this.el = node;
 
-                        rect = node.append('rect');
+                        // Label
+                        var labelElements = this.addLabelElements(label);
+                        labelWidth = label ? this.computeLabelWidth(node) : 0;
+                        labelElements.rect.attr(iconBox(devIconDim, labelWidth));
 
-                        text = node.append('text').text(label)
-                            .attr('text-anchor', 'left')
-                            .attr('y', '0.3em')
-                            .attr('x', halfDevIcon + labelPad);
-
+                        // Icon
                         glyph = is.addDeviceIcon(node, glyphId, devIconDim);
-
-                        labelWidth = label ? computeLabelWidth(node) : 0;
-
-                        rect.attr(iconBox(devIconDim, labelWidth));
                         glyph.attr(iconBox(devIconDim, 0));
 
                         node.attr('transform', sus.translate(-halfDevIcon, -halfDevIcon));
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 8cfaadf..d2a49ea 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -193,7 +193,8 @@
             .transition()
             .attr('opacity', 1);
 
-        entering.filter('.device').each(t2d3.deviceEnter);
+        entering.filter('.device').each(t2d3.nodeEnter);
+        entering.filter('.sub-region').each(t2d3.nodeEnter);
         entering.filter('.host').each(t2d3.hostEnter);
 
         // operate on both existing and new nodes:
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 44c5ec9..b7c3ea6 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -22,6 +22,7 @@
 (function () {
     'use strict';
 
+    var $log;
     var Collection, Model, region, ts;
 
     var widthRatio = 1.4,
@@ -55,7 +56,6 @@
     function createLink() {
 
         var linkPoints = this.linkEndPoints(this.get('epA'), this.get('epB'));
-        console.log(this);
 
         var attrs = angular.extend({}, linkPoints, {
             key: this.get('id'),
@@ -78,19 +78,14 @@
 
     function linkEndPoints(srcId, dstId) {
 
-        var sourceNode = this.region.get('devices').get(srcId.substring(0, srcId.length -2));
-        var targetNode = this.region.get('devices').get(dstId.substring(0, dstId.length -2));
+        var sourceNode = this.region.findNodeById(srcId)
+        var targetNode = this.region.findNodeById(dstId)
 
-//        var srcNode = lu[srcId],
-//            dstNode = lu[dstId],
-//            sMiss = !srcNode ? missMsg('src', srcId) : '',
-//            dMiss = !dstNode ? missMsg('dst', dstId) : '';
-//
-//        if (sMiss || dMiss) {
-//            $log.error('Node(s) not on map for link:' + sMiss + dMiss);
-//            //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
-//            return null;
-//        }
+        if (!sourceNode || !targetNode) {
+            $log.error('Node(s) not on map for link:' + srcId + ':' + dstId);
+            //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
+            return null;
+        }
 
         this.source = sourceNode.toJSON();
         this.target = targetNode.toJSON();
@@ -118,31 +113,17 @@
                 return true;
                 return both && (s && s.online) && (t && t.online);
             },
-            linkWidth: function () {
-                var s = this.get('fromSource'),
-                    t = this.get('fromTarget'),
-                    ws = (s && s.linkWidth) || 0,
-                    wt = (t && t.linkWidth) || 0;
-
-                    // console.log(s);
-                // TODO: Current json is missing linkWidth
-                return 1.2;
-                return this.get('position').multiLink ? 5 : Math.max(ws, wt);
-            },
-
             restyleLinkElement: function (immediate) {
                 // this fn's job is to look at raw links and decide what svg classes
                 // need to be applied to the line element in the DOM
                 var th = ts.theme(),
                     el = this.el,
                     type = this.get('type'),
-                    lw = this.linkWidth(),
                     online = this.online(),
                     modeCls = this.expected() ? 'inactive' : 'not-permitted',
+                    lw = 1.2,
                     delay = immediate ? 0 : 1000;
 
-                console.log(type);
-
                 // NOTE: understand why el is sometimes undefined on addLink events...
                 // Investigated:
                 // el is undefined when it's a reverse link that is being added.
@@ -185,10 +166,11 @@
 
     angular.module('ovTopo2')
     .factory('Topo2LinkService',
-        ['Topo2Collection', 'Topo2Model', 'ThemeService',
+        ['$log', 'Topo2Collection', 'Topo2Model', 'ThemeService',
 
-            function (_Collection_, _Model_, _ts_) {
+            function (_$log_, _Collection_, _Model_, _ts_) {
 
+                $log = _$log_;
                 ts = _ts_;
                 Collection = _Collection_;
                 Model = _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 54a2748..0e617dd 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
@@ -29,6 +29,11 @@
     var defaultLinkType = 'direct',
         nearDist = 15;
 
+    var devIconDim = 36,
+        labelPad = 10,
+        halfDevIcon = devIconDim / 2,
+        nodeLabelIndex = 1;
+
     function positionNode(node, forUpdate) {
 
         var meta = node.metaUi,
@@ -113,6 +118,36 @@
                 initialize: function () {
                     this.node = this.createNode();
                 },
+                label: function () {
+
+                    var props = this.get('props'),
+                        id = this.get('id'),
+                        friendlyName = props ? props.name : id,
+                        labels = ['', friendlyName, id],
+                        idx = (nodeLabelIndex < labels.length) ? nodeLabelIndex : 0;
+
+                    return labels[idx];
+                },
+                trimLabel: function(label) {
+                    return (label && label.trim()) || '';
+                },
+                computeLabelWidth: function(el) {
+                    var text = el.select('text'),
+                    box = text.node().getBBox();
+                    return box.width + labelPad * 2;
+                },
+                addLabelElements: function(label) {
+                    var rect = this.el.append('rect');
+                    var text = this.el.append('text').text(label)
+                        .attr('text-anchor', 'left')
+                        .attr('y', '0.3em')
+                        .attr('x', halfDevIcon + labelPad);
+
+                    return {
+                        rect: rect,
+                        text: text
+                    }
+                },
                 svgClassName: function () {
                     return fn.classNames('node', this.nodeType, this.get('type'), {
                         online: this.get('online')
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 45c2652..67fec91 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -22,14 +22,11 @@
 (function () {
     'use strict';
 
-    var $log,
-        wss,
-        Model,
-        t2sr,
-        t2ds,
-        t2hs,
-        t2ls;
+    // Injected Services
+    var $log, wss, t2sr, t2ds, t2hs, t2ls;
+    var Collection, Model;
 
+    //Internal
     var region;
 
     function init() {
@@ -38,9 +35,13 @@
 
     function addRegion(data) {
 
-        region = new Model({
+        var RegionModel = Model.extend({
+            findNodeById: findNodeById
+        })
+
+        region = new RegionModel({
             id: data.id,
-            layerOrder: data.layerOrder
+            layerOrder: data.layerOrder,
         });
 
         region.set({
@@ -50,8 +51,6 @@
             links: t2ls.createLinkCollection(data.links, region),
         });
 
-        region.set('test', 2);
-
         angular.forEach(region.get('links').models, function (link) {
             link.createLink();
         });
@@ -60,9 +59,23 @@
     }
 
     function regionNodes() {
-        return [].concat(region.get('devices').models, region.get('hosts').models);
+        return [].concat(
+            region.get('devices').models,
+            region.get('hosts').models,
+            region.get('subregions').models
+        );
     }
 
+    function findNodeById(id) {
+
+        // 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 regionLinks() {
         return region.get('links').models;
@@ -71,9 +84,9 @@
     angular.module('ovTopo2')
     .factory('Topo2RegionService',
         ['$log', 'WebSocketService', 'Topo2Model', 'Topo2SubRegionService', 'Topo2DeviceService',
-        'Topo2HostService', 'Topo2LinkService',
+        'Topo2HostService', 'Topo2LinkService', 'Topo2Collection',
 
-        function (_$log_, _wss_, _Model_, _t2sr_, _t2ds_, _t2hs_, _t2ls_) {
+        function (_$log_, _wss_, _Model_, _t2sr_, _t2ds_, _t2hs_, _t2ls_, _Collection_) {
 
             $log = _$log_;
             wss = _wss_;
@@ -82,6 +95,7 @@
             t2ds = _t2ds_;
             t2hs = _t2hs_;
             t2ls = _t2ls_;
+            Collection = _Collection_;
 
             return {
                 init: init,
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 421f57d..016c7b4 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,30 @@
 (function () {
     'use strict';
 
-    var Collection, Model;
+    var Collection, Model, is, sus, ts, t2vs;
+
+    var remappedDeviceTypes = {
+        virtual: 'cord'
+    };
+
+    // configuration
+    var devIconDim = 36,
+        labelPad = 10,
+        hostRadius = 14,
+        badgeConfig = {
+            radius: 12,
+            yoff: 5,
+            gdelta: 10
+        },
+        halfDevIcon = devIconDim / 2,
+        devBadgeOff = { dx: -halfDevIcon, dy: -halfDevIcon },
+        hostBadgeOff = { dx: -hostRadius, dy: -hostRadius },
+        status = {
+            i: 'badgeInfo',
+            w: 'badgeWarn',
+            e: 'badgeError'
+        },
+        deviceLabelIndex = 0;
 
     function createSubRegionCollection(data, region) {
 
@@ -33,14 +56,63 @@
         return new SubRegionCollection(data);
     }
 
+    function mapDeviceTypeToGlyph(type) {
+        return remappedDeviceTypes[type] || type || 'switch';
+    }
+
+    function iconBox(dim, labelWidth) {
+        return {
+            x: -dim / 2,
+            y: -dim / 2,
+            width: dim + labelWidth,
+            height: dim
+        }
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2SubRegionService',
-        ['Topo2Collection', 'Topo2Model',
+        ['Topo2Collection', 'Topo2NodeModel', 'IconService', 'SvgUtilService',
+        'ThemeService', 'Topo2ViewService',
 
-            function (_Collection_, _Model_) {
+            function (_Collection_, _NodeModel_, _is_, _sus_, _ts_, classnames, _t2vs_) {
 
+                t2vs = _t2vs_;
+                is = _is_;
+                sus = _sus_;
+                ts = _ts_;
                 Collection = _Collection_;
-                Model = _Model_.extend({});
+
+                Model = _NodeModel_.extend({
+                    initialize: function () {
+                        this.set('weight', 0);
+                        this.constructor.__super__.initialize.apply(this, arguments);
+                    },
+                    nodeType: 'sub-region',
+                    mapDeviceTypeToGlyph: mapDeviceTypeToGlyph,
+                    onEnter: function (el) {
+
+                        var node = d3.select(el),
+                            glyphId = mapDeviceTypeToGlyph(this.get('type')),
+                            label = this.trimLabel(this.label()),
+                            glyph, labelWidth;
+
+                        this.el = node;
+
+                        // Label
+                        var labelElements = this.addLabelElements(label);
+                        labelWidth = label ? this.computeLabelWidth(node) : 0;
+                        labelElements.rect.attr(iconBox(devIconDim, labelWidth));
+
+                        // Icon
+                        glyph = is.addDeviceIcon(node, glyphId, devIconDim);
+                        glyph.attr(iconBox(devIconDim, 0));
+
+                        node.attr('transform', sus.translate(-halfDevIcon, -halfDevIcon));
+                        this.render();
+                    },
+                    onExit: function () {},
+                    render: function () {}
+                });
 
                 return {
                     createSubRegionCollection: createSubRegionCollection