Topo2: Adding peer region node to the topology

Change-Id: I846d2f1ca27faa4602c772aba006f5be55da6106
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 7ce65ad..e3d0c90 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
@@ -187,6 +187,10 @@
     fill: #454545;
 }
 
+#ov-topo2 svg .node.peer-region rect {
+    fill: #ffffff;
+}
+
 #ov-topo2 svg .node.selected .node-container {
     stroke-width: 2.0;
     stroke: #009fdb;
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Background.js b/web/gui/src/main/webapp/app/view/topo2/topo2Background.js
index 611273b..c90b06d 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Background.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Background.js
@@ -28,8 +28,8 @@
     angular.module('ovTopo2')
         .factory('Topo2BackgroundService', [
             '$log', 'Topo2ViewController', 'Topo2SpriteLayerService', 'Topo2MapService',
-            'Topo2MapConfigService', 'Topo2RegionService',
-            function (_$log_, ViewController, t2sls, t2ms, t2mcs, t2rs) {
+            'Topo2MapConfigService',
+            function (_$log_, ViewController, t2sls, t2ms, t2mcs) {
 
                 $log = _$log_;
 
@@ -45,10 +45,14 @@
                         t2ms.init();
                     },
                     addLayout: function (data) {
-                        this.background = data;
-                        t2rs.bgRendered = false;
 
-                        if (data.bgType === 'geo') {
+                        var _this = this;
+
+                        this.background = data;
+                        this.bgType = data.bgType;
+                        _this.region.loaded('bgRendered', false);
+
+                        if (this.bgType === 'geo') {
 
                             // Hide Sprite Layer and show Map
                             t2sls.hide();
@@ -62,20 +66,24 @@
                                 t2mcs.projection(proj);
                                 // $log.debug('** Zoom restored:', z);
                                 $log.debug('** We installed the projection:', proj);
-                                t2rs.backgroundRendered();
+                                _this.region.loaded('bgRendered', true);
                             });
                         }
 
-                        if (data.bgType === 'grid') {
+                        if (this.bgType === 'grid') {
 
                             // Hide Sprite Layer and show Map
                             t2ms.hide();
                             t2sls.show();
 
-                            t2sls.loadLayout(data.bgId).then(function () {
-                                t2rs.backgroundRendered();
+                            t2sls.loadLayout(data.bgId).then(function (spriteLayout) {
+                                _this.background.layout = spriteLayout;
+                                _this.region.loaded('bgRendered', true);
                             });
                         }
+                    },
+                    getBackgroundType: function () {
+                        return this.bgType;
                     }
                 });
 
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 b49de81..5472f65 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
@@ -44,8 +44,7 @@
         model: Model,
         addModel: function (data) {
             var CollectionModel = this.model;
-            var model = new CollectionModel(data);
-            model.collection = this;
+            var model = new CollectionModel(data, this);
 
             this.models.push(model);
             this._byId[data.id] = model;
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 932101c..e036020 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
@@ -30,14 +30,6 @@
         node.onExit(this, node);
     }
 
-    function hostEnter(node) {
-        node.onEnter(this, node);
-    }
-
-    function linkEntering(link) {
-        link.onEnter(this);
-    }
-
     angular.module('ovTopo2')
     .factory('Topo2D3Service', [
 
@@ -45,8 +37,6 @@
             return {
                 nodeEnter: nodeEnter,
                 nodeExit: nodeExit,
-                hostEnter: hostEnter,
-                linkEntering: linkEntering
             };
         }
     ]
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 78f37a3..e1e392b 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
@@ -43,6 +43,7 @@
 
 
         t2bgs.init();
+        t2bgs.region = t2rs;
         t2ls.init(svg, uplink, dim, zoomer, opts);
         t2bcs.addLayout(t2ls);
         t2rs.layout = t2ls;
@@ -83,11 +84,12 @@
 
     function currentRegion(data) {
         $log.debug('>> topo2CurrentRegion event:', data);
-        t2rs.addRegion(data);
+        t2rs.loaded('regionData', data);
     }
 
     function topo2PeerRegions(data) {
         $log.debug('>> topo2PeerRegions event:', data);
+        t2rs.loaded('peers', data.peers);
     }
 
     function modelEvent(data) {
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 36a5ced..4e21040 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -136,8 +136,7 @@
                         return this.settings[settingName][nodeType] || this.settings[settingName]._def_;
                     },
                     createForceLayout: function () {
-                        var _this = this,
-                            regionLinks = t2rs.regionLinks(),
+                        var regionLinks = t2rs.regionLinks(),
                             regionNodes = t2rs.regionNodes();
 
                         this.force = d3.layout.force()
@@ -150,14 +149,6 @@
                             .nodes(regionNodes)
                             .links(regionLinks)
                             .on("tick", this.tick.bind(this))
-                            .on("start", function () {
-
-                                // TODO: Find a better way to do this
-                                // TODO: BROKEN - Click and dragging and element triggers this event
-//                                setTimeout(function () {
-//                                    _this.centerLayout();
-//                                }, 500);
-                            })
                             .start();
 
                         this.link = this.elements.linkG.selectAll('.link')
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 e644c7c..7c674c3 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -117,6 +117,11 @@
     function createLinkCollection(data, _region) {
 
         var LinkModel = Model.extend({
+            initialize: function () {
+                this.super = this.constructor.__super__;
+                this.super.initialize.apply(this, arguments);
+                this.createLink();
+            },
             region: _region,
             createLink: createLink,
             linkEndPoints: linkEndPoints,
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 c8430fb..31ec0f6 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
@@ -22,13 +22,14 @@
 (function () {
     'use strict';
 
-    function Model(attributes) {
+    function Model(attributes, collection) {
 
         var attrs = attributes || {};
         this.attributes = {};
 
         attrs = angular.extend({}, attrs);
         this.set(attrs, { silent: true });
+        this.collection = collection;
         this.initialize.apply(this, arguments);
     }
 
@@ -82,11 +83,11 @@
 
                 val = attribute;
 
-                if (!angular.equals(current[index], val)) {
+                if (!_.isEqual(current[index], val)) {
                     changes.push(index);
                 }
 
-                if (angular.equals(previous[index], val)) {
+                if (!_.isEqual(previous[index], val)) {
                     delete changed[index];
                 } else {
                     changed[index] = val;
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 94ab15f..9d46494 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
@@ -93,6 +93,17 @@
 
                     return selected;
                 },
+                index: function () {
+
+                    var models = this.collection.models,
+                        id = this.get('id');
+
+                    var index = _.find(models, function (model, i) {
+                        return model.get('id') === id;
+                    });
+
+                    return index || models.length;
+                },
                 deselect: function () {
                     this.set('selected', false);
                 },
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2NodePosition.js b/web/gui/src/main/webapp/app/view/topo2/topo2NodePosition.js
index 2c78a00..af708fb 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2NodePosition.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodePosition.js
@@ -23,18 +23,39 @@
     'use strict';
 
     // Injected vars
-    var rs, t2mcs, t2sls;
+    var rs, t2mcs, t2sls, t2bgs;
 
     // Internal state;
     var nearDist = 15;
 
     function positionNode(node, forUpdate) {
+
         var meta = node.get('metaUi'),
             x = meta && meta.x,
             y = meta && meta.y,
             dim = [800, 600],
             xy;
 
+        if (node.nodeType === 'peer-region') {
+
+            var coord = [0, 0],
+                loc = {};
+
+            if (t2bgs.getBackgroundType() === 'geo') {
+                // TODO: Set coords for geo (lat/long)
+            } else {
+                loc.gridX = -20;
+                loc.gridY = 10 * node.index();
+                coord = coordFromXY(loc);
+            }
+
+            node.px = node.x = coord[0];
+            node.py = node.y = coord[1];
+
+            node.fix(true);
+            return;
+        }
+
         // If the device contains explicit LONG/LAT data, use that to position
         if (setLongLat(node)) {
             // Indicate we want to update cached meta data...
@@ -146,12 +167,13 @@
 
     angular.module('ovTopo2')
     .factory('Topo2NodePositionService',
-        ['RandomService', 'Topo2MapConfigService', 'Topo2SpriteLayerService',
-            function (_rs_, _t2mcs_, _t2sls_) {
+        ['RandomService', 'Topo2MapConfigService', 'Topo2SpriteLayerService', 'Topo2BackgroundService',
+            function (_rs_, _t2mcs_, _t2sls_, _t2bgs_) {
 
                 rs = _rs_;
                 t2mcs = _t2mcs_;
                 t2sls = _t2sls_;
+                t2bgs = _t2bgs_;
 
                 return {
                     positionNode: positionNode,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js b/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js
new file mode 100644
index 0000000..2ae8e64
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology PeerRegion Module.
+ Module that creates a peer region node for the topology
+ */
+
+(function () {
+    'use strict';
+
+    var Collection, Model;
+
+    var remappedDeviceTypes = {
+        virtual: 'cord'
+    };
+
+    function createCollection(data, region) {
+
+        var PeerRegionCollection = Collection.extend({
+            model: Model,
+            region: region
+        });
+
+        return new PeerRegionCollection(data);
+    }
+
+    angular.module('ovTopo2')
+        .factory('Topo2PeerRegionService', [
+            'WebSocketService', 'Topo2Collection', 'Topo2NodeModel',
+            'Topo2SubRegionPanelService',
+
+            function (wss, _c_, NodeModel, t2srp) {
+
+                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: 'peer-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')
+                        });
+
+                        var layout = this.collection.region.layout;
+                        layout.createForceElements();
+                        layout.transitionDownRegion();
+
+                        t2srp.hide();
+                    }
+                });
+
+                return {
+                    createCollection: createCollection
+                };
+            }
+        ]);
+
+})();
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 5895871..67b2a7c 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -36,9 +36,9 @@
         '$log', 'Topo2Model', 'Topo2SubRegionService', 'Topo2DeviceService',
         'Topo2HostService', 'Topo2LinkService', 'Topo2ZoomService', 'Topo2DetailsPanelService',
         'Topo2BreadcrumbService', 'Topo2ViewController', 'Topo2SpriteLayerService', 'Topo2MapService',
-        'Topo2MapConfigService',
+        'Topo2MapConfigService', 'Topo2PeerRegionService',
         function ($log, _Model_, t2sr, t2ds, t2hs, t2ls, t2zs, t2dps, t2bcs, ViewController,
-                  t2sls, t2ms, t2mcs) {
+                  t2sls, t2ms, t2mcs, t2pr) {
 
             Model = _Model_;
 
@@ -46,7 +46,10 @@
                 initialize: function () {
                     instance = this;
                     this.model = null;
+
                     this.bgRendered = false;
+                    this.regionData = null;
+                    this.peers = null;
 
                     var RegionModel = Model.extend({
                         findNodeById: this.findNodeById,
@@ -55,35 +58,26 @@
 
                     this.model = new RegionModel();
                 },
-                backgroundRendered: function () {
-                    this.bgRendered = true;
-
-                    if (this.regionData) {
-                        this.startRegion();
-                    }
-                },
-                addRegion: function (data) {
-                    this.clear();
-                    this.regionData = data;
-
-                    if (this.bgRendered) {
+                loaded: function (key, value) {
+                    this[key] = value;
+                    if (this.bgRendered && this.regionData && this.peers) {
                         this.startRegion();
                     }
                 },
                 startRegion: function () {
 
+                    var _this = this;
+
                     this.model.set({
                         id: this.regionData.id,
-                        layerOrder: this.regionData.layerOrder,
-                        subregions: t2sr.createSubRegionCollection(this.regionData.subregions, this),
-                        devices: t2ds.createDeviceCollection(this.regionData.devices, this),
-                        hosts: t2hs.createHostCollection(this.regionData.hosts, this),
-                        links: t2ls.createLinkCollection(this.regionData.links, this)
+                        layerOrder: this.regionData.layerOrder
                     });
 
-                    angular.forEach(this.model.get('links').models, function (link) {
-                        link.createLink();
-                    });
+                    this.model.set({ subregions: t2sr.createSubRegionCollection(this.regionData.subregions, this) });
+                    this.model.set({ devices: t2ds.createDeviceCollection(this.regionData.devices, this) });
+                    this.model.set({ hosts: t2hs.createHostCollection(this.regionData.hosts, this) });
+                    this.model.set({ peerRegions: t2pr.createCollection(this.peers, this) });
+                    this.model.set({ links: t2ls.createLinkCollection(this.regionData.links, this) });
 
                     // Hide Breadcrumbs if there are no subregions configured in the root region
                     if (this.isRootRegion() && !this.model.get('subregions').models.length) {
@@ -98,15 +92,6 @@
 
                     if (!this.model)
                         return;
-
-                    this.model.set({
-                        id: null,
-                        layerOrder: null,
-                        subregions: t2sr.createSubRegionCollection([], this),
-                        devices: t2ds.createDeviceCollection([], this),
-                        hosts: t2hs.createHostCollection([], this),
-                        links: t2ls.createLinkCollection([], this)
-                    });
                 },
                 isRootRegion: function () {
                     return this.model.get('id') === ROOT;
@@ -126,7 +111,8 @@
                         return [].concat(
                             this.model.get('devices').models,
                             this.model.get('hosts').models,
-                            this.model.get('subregions').models
+                            this.model.get('subregions').models,
+                            this.model.get('peerRegions').models
                         );
                     }
                     return [];
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2SpriteLayer.js b/web/gui/src/main/webapp/app/view/topo2/topo2SpriteLayer.js
index 6de99fe..b3452c2 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2SpriteLayer.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SpriteLayer.js
@@ -46,6 +46,7 @@
                         this.container = this.appendElement('#topo2-background', 'g');
                     },
                     loadLayout: function (id) {
+                        var _this = this;
                         this.container.selectAll("*").remove();
                         this.layout = ss.layout(id);
 
@@ -60,7 +61,7 @@
 
                         // Returns a promise for consistency with Topo2MapService
                         return new Promise(function(resolve) {
-                            resolve();
+                            resolve(_this);
                         });
                     },
                     createSpriteDefs: function () {