Topo2: Refactored NavigateToRegion events

There was duplicated logic in Topo2Breadcrumb|SubRegion|PeerRegion. This has been refactored out so
all the logic for navigating to a new region is handled in a single place (Topo2RegionNavigationService).

Topo2RegionNavigationService is a pub/sub class. There are two reasons for this choice;
1. Topo2Layout needs to create the new force layout simulation for the region that is about to enter.   Then it creates a transition between the old and new.
2. This will allow application developers to hook into the region:navigation event incase they need
   to update the topology for the new region displayed.

Change-Id: Ie8dad531b50afe2e4735086395c1285fb982122e
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 49a68a1..d2a1391 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Breadcrumb.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Breadcrumb.js
@@ -23,7 +23,7 @@
 
     'use strict';
 
-    var $log, $loc, wss;
+    var $log, $loc, wss, t2rns;
 
     // Internal
     var breadcrumbContainer,
@@ -48,29 +48,18 @@
     }
 
     function navigateToRegion(data, index) {
-
         if (index === breadcrumbs.length - 1) {
             return;
         }
 
         // Remove breadcrumbs after index;
         breadcrumbs.splice(index + 1);
-
-        // TODO: This is duplicated code - See Topo2SubRegion:navigateToRegion()
-        wss.sendEvent('topo2navRegion', {
-            rid: data.id
-        });
-
-        $loc.search('regionId', data.id);
-
-        layout.createForceElements();
-        layout.transitionDownRegion();
+        t2rns.navigateToRegion(data.id);
 
         render();
     }
 
     function render() {
-
         var selection = breadcrumbContainer.selectAll('.breadcrumb')
             .data(breadcrumbs);
 
@@ -91,7 +80,6 @@
     }
 
     function hide() {
-
         var view = d3.select('body');
         view.classed('breadcrumb--hidden', true);
 
@@ -115,11 +103,13 @@
     angular.module('ovTopo2')
     .factory('Topo2BreadcrumbService', [
         '$log', '$location', 'WebSocketService',
-        function (_$log_, _$loc_, _wss_) {
+        'Topo2RegionNavigationService',
+        function (_$log_, _$loc_, _wss_, _t2rns_) {
 
             $log = _$log_;
             $loc = _$loc_;
             wss = _wss_;
+            t2rns = _t2rns_;
 
             return {
                 init: init,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
index 2a8e95d..c5e8251 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -80,14 +80,17 @@
         [
             '$log', '$timeout', 'WebSocketService', 'SvgUtilService', 'Topo2RegionService',
             'Topo2ViewService', 'Topo2SelectService', 'Topo2ZoomService',
-            'Topo2ViewController',
+            'Topo2ViewController', 'Topo2RegionNavigationService',
             function ($log, $timeout, wss, sus, t2rs, t2vs, t2ss, t2zs,
-                      ViewController) {
+                      ViewController, t2rns) {
 
                 var Layout = ViewController.extend({
                     init: function (svg, forceG, uplink, dim, zoomer, opts) {
                         instance = this;
 
+                        var navToRegion = this.navigateToRegionHandler.bind(this);
+                        t2rns.addListener('region:navigation-start', navToRegion);
+
                         this.svg = svg;
 
                         // Append all the SVG Group elements to the forceG object
@@ -318,7 +321,10 @@
                             .transition()
                             .delay(500)
                             .duration(500)
-                            .style('opacity', 1);
+                            .style('opacity', 1)
+                            .each('end', function () {
+                                t2rns.navigateToRegionComplete();
+                            });
                     },
                     transitionUpRegion: function () {
                         this.prevForce.transition()
@@ -331,7 +337,14 @@
                             .transition()
                             .delay(500)
                             .duration(500)
-                            .style('opacity', 1);
+                            .style('opacity', 1)
+                            .each('end', function () {
+                                t2rns.navigateToRegionComplete();
+                            });;
+                    },
+                    navigateToRegionHandler: function () {
+                        this.createForceElements();
+                        this.transitionDownRegion();
                     }
                 });
 
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js b/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js
index 469e0a8..9e8ad31 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2PeerRegion.js
@@ -41,9 +41,9 @@
     angular.module('ovTopo2')
         .factory('Topo2PeerRegionService', [
             'WebSocketService', 'Topo2Collection', 'Topo2NodeModel',
-            'Topo2SubRegionPanelService',
+            'Topo2SubRegionPanelService', 'Topo2RegionNavigationService',
 
-            function (wss, _c_, NodeModel, t2srp) {
+            function (wss, _c_, NodeModel, t2srp, t2rns) {
 
                 Collection = _c_;
 
@@ -73,17 +73,8 @@
                         return remappedDeviceTypes[type] || type || 'm_cloud';
                     },
                     navigateToRegion: function () {
-
                         if (d3.event.defaultPrevented) return;
-
-                        wss.sendEvent('topo2navRegion', {
-                            rid: this.get('id')
-                        });
-
-                        var layout = this.collection.region.layout;
-                        layout.createForceElements();
-                        layout.transitionDownRegion();
-
+                        t2rns.navigateToRegion(this.get('id'));
                         t2srp.hide();
                     }
                 });
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2RegionNavigation.js b/web/gui/src/main/webapp/app/view/topo2/topo2RegionNavigation.js
new file mode 100644
index 0000000..092ec2c
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2RegionNavigation.js
@@ -0,0 +1,77 @@
+/*
+ * 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 Region Navigation Service
+ */
+
+(function () {
+
+    var $loc, wss;
+    var instance;
+
+    var RegionNavigationService = function () {
+        this.listeners = {};
+        instance = this;
+    };
+
+    RegionNavigationService.prototype = {
+
+        addListener: function (event, sub) {
+            if (!this.listeners[event]) {
+                this.listeners[event] = [];
+            }
+            this.listeners[event].push(sub);
+        },
+        removeListener: function (sub) {
+            // TODO: This will be needed when we re-implement the overlays
+            // An overlay might want to add a listener when activated and will
+            // need to remove the listener when deactivated.
+        },
+        notifyListeners: function (event, payload) {
+            _.each(this.listeners[event], function (cb) {
+                cb(payload);
+            });
+        },
+
+        navigateToRegion: function (id) {
+            $loc.search('regionId', id);
+            wss.sendEvent('topo2navRegion', {
+                rid: id
+            });
+            this.notifyListeners('region:navigation-start', id);
+        },
+        navigateToRegionComplete: function () {
+            this.notifyListeners('region:navigation-complete');
+        },
+
+        destory: function () {
+            this.listeners = {};
+        }
+    };
+
+    angular.module('ovTopo2')
+        .factory('Topo2RegionNavigationService', [
+            '$location', 'WebSocketService',
+            function (_$loc_, _wss_) {
+
+                $loc = _$loc_;
+                wss = _wss_;
+
+                return instance || new RegionNavigationService();
+            }
+        ]);
+})();
\ No newline at end of file
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 c773539..c426d64 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
@@ -41,9 +41,9 @@
     angular.module('ovTopo2')
     .factory('Topo2SubRegionService', [
         '$location', 'WebSocketService', 'Topo2Collection', 'Topo2NodeModel',
-        'Topo2SubRegionPanelService',
+        'Topo2SubRegionPanelService', 'Topo2RegionNavigationService',
 
-        function ($loc, wss, _c_, NodeModel, t2srp) {
+        function ($loc, wss, _c_, NodeModel, t2srp, t2rns) {
 
             Collection = _c_;
 
@@ -73,19 +73,8 @@
                     return remappedDeviceTypes[type] || type || 'm_cloud';
                 },
                 navigateToRegion: function () {
-
                     if (d3.event.defaultPrevented) return;
-
-                    wss.sendEvent('topo2navRegion', {
-                        rid: this.get('id')
-                    });
-
-                    $loc.search('regionId', this.get('id'));
-
-                    var layout = this.collection.region.layout;
-                    layout.createForceElements();
-                    layout.transitionDownRegion();
-
+                    t2rns.navigateToRegion(this.get('id'));
                     t2srp.hide();
                 }
             });
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index d778f39..7ca688d 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -158,6 +158,7 @@
     <script src="app/view/topo2/topo2PeerRegion.js"></script>
     <script src="app/view/topo2/topo2Prefs.js"></script>
     <script src="app/view/topo2/topo2Region.js"></script>
+    <script src="app/view/topo2/topo2RegionNavigation.js"></script>
     <script src="app/view/topo2/topo2Select.js"></script>
     <script src="app/view/topo2/topo2SpriteLayer.js"></script>
     <script src="app/view/topo2/topo2SummaryPanel.js"></script>