GUI -- TopoView - Migrated more helper functions to topoModel.js.

Change-Id: I902c3561210c46fd23c6f6f01323d003dacefc19
diff --git a/web/gui/src/main/webapp/app/view/topo/topoEvent.js b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
index bbe3d3a..887fe4a 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoEvent.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
@@ -61,7 +61,7 @@
                 eh = api[eid];
 
             if (eh) {
-                $log.debug('  *EVENT* ', ev.payload);
+                $log.debug('  *EVENT* ', eid, ev.payload);
                 eh(ev.payload);
             } else {
                 $log.warn('Unknown event (ignored):', ev);
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index e29c6c0..703212f 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -70,6 +70,7 @@
             revLinkToKey: {}
         },
         lu = network.lookup,    // shorthand
+        rlk = network.revLinkToKey,
         deviceLabelIndex = 0,   // for device label cycling
         hostLabelIndex = 0,     // for host label cycling
         showHosts = true,       // whether hosts are displayed
@@ -134,9 +135,6 @@
         d = tms.createDeviceNode(data);
         network.nodes.push(d);
         lu[id] = d;
-
-        $log.debug("Created new device.. ", d.id, d.x, d.y);
-
         updateNodes();
         fStart();
     }
@@ -154,7 +152,7 @@
             }
             updateNodes();
             if (wasOnline !== d.online) {
-                findAttachedLinks(d.id).forEach(restyleLinkElement);
+                tms.findAttachedLinks(d.id).forEach(restyleLinkElement);
                 updateOfflineVisibility(d);
             }
         } else {
@@ -188,16 +186,10 @@
         d = tms.createHostNode(data);
         network.nodes.push(d);
         lu[id] = d;
-
-        $log.debug("Created new host.. ", d.id, d.x, d.y);
-
         updateNodes();
 
         lnk = tms.createHostLink(data);
         if (lnk) {
-
-            $log.debug("Created new host-link.. ", lnk.key);
-
             d.linkData = lnk;    // cache ref on its host
             network.links.push(lnk);
             lu[d.ingress] = lnk;
@@ -235,7 +227,7 @@
     }
 
     function addLink(data) {
-        var result = findLink(data, 'add'),
+        var result = tms.findLink(data, 'add'),
             bad = result.badLogic,
             d = result.ldata;
 
@@ -261,7 +253,7 @@
     }
 
     function updateLink(data) {
-        var result = findLink(data, 'update'),
+        var result = tms.findLink(data, 'update'),
             bad = result.badLogic;
         if (bad) {
             //logicError(bad + ': ' + link.id);
@@ -271,7 +263,7 @@
     }
 
     function removeLink(data) {
-        var result = findLink(data, 'remove'),
+        var result = tms.findLink(data, 'remove'),
             bad = result.badLogic;
         if (bad) {
             // may have already removed link, if attached to removed device
@@ -286,20 +278,10 @@
     function addLinkUpdate(ldata, link) {
         // add link event, but we already have the reverse link installed
         ldata.fromTarget = link;
-        network.revLinkToKey[link.id] = ldata.key;
+        rlk[link.id] = ldata.key;
         restyleLinkElement(ldata);
     }
 
-    function makeNodeKey(d, what) {
-        var port = what + 'Port';
-        return d[what] + '/' + d[port];
-    }
-
-    function makeLinkKey(d, flipped) {
-        var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
-            two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
-        return one + '-' + two;
-    }
 
     var widthRatio = 1.4,
         linkScale = d3.scale.linear()
@@ -329,113 +311,7 @@
             .attr('stroke', linkConfig[th].baseColor);
     }
 
-    function findLinkById(id) {
-        // check to see if this is a reverse lookup, else default to given id
-        var key = network.revLinkToKey[id] || id;
-        return key && lu[key];
-    }
 
-    function findLink(linkData, op) {
-        var key = makeLinkKey(linkData),
-            keyrev = makeLinkKey(linkData, 1),
-            link = lu[key],
-            linkRev = lu[keyrev],
-            result = {},
-            ldata = link || linkRev,
-            rawLink;
-
-        if (op === 'add') {
-            if (link) {
-                // trying to add a link that we already know about
-                result.ldata = link;
-                result.badLogic = 'addLink: link already added';
-
-            } else if (linkRev) {
-                // we found the reverse of the link to be added
-                result.ldata = linkRev;
-                if (linkRev.fromTarget) {
-                    result.badLogic = 'addLink: link already added';
-                }
-            }
-        } else if (op === 'update') {
-            if (!ldata) {
-                result.badLogic = 'updateLink: link not found';
-            } else {
-                rawLink = link ? ldata.fromSource : ldata.fromTarget;
-                result.updateWith = function (data) {
-                    angular.extend(rawLink, data);
-                    restyleLinkElement(ldata);
-                }
-            }
-        } else if (op === 'remove') {
-            if (!ldata) {
-                result.badLogic = 'removeLink: link not found';
-            } else {
-                rawLink = link ? ldata.fromSource : ldata.fromTarget;
-
-                if (!rawLink) {
-                    result.badLogic = 'removeLink: link not found';
-
-                } else {
-                    result.removeRawLink = function () {
-                        if (link) {
-                            // remove fromSource
-                            ldata.fromSource = null;
-                            if (ldata.fromTarget) {
-                                // promote target into source position
-                                ldata.fromSource = ldata.fromTarget;
-                                ldata.fromTarget = null;
-                                ldata.key = keyrev;
-                                delete network.lookup[key];
-                                network.lookup[keyrev] = ldata;
-                                delete network.revLinkToKey[keyrev];
-                            }
-                        } else {
-                            // remove fromTarget
-                            ldata.fromTarget = null;
-                            delete network.revLinkToKey[keyrev];
-                        }
-                        if (ldata.fromSource) {
-                            restyleLinkElement(ldata);
-                        } else {
-                            removeLinkElement(ldata);
-                        }
-                    }
-                }
-            }
-        }
-        return result;
-    }
-
-    function findDevices(offlineOnly) {
-        var a = [];
-        network.nodes.forEach(function (d) {
-            if (d.class === 'device' && !(offlineOnly && d.online)) {
-                a.push(d);
-            }
-        });
-        return a;
-    }
-
-    function findAttachedHosts(devId) {
-        var hosts = [];
-        network.nodes.forEach(function (d) {
-            if (d.class === 'host' && d.cp.device === devId) {
-                hosts.push(d);
-            }
-        });
-        return hosts;
-    }
-
-    function findAttachedLinks(devId) {
-        var links = [];
-        network.links.forEach(function (d) {
-            if (d.source.id === devId || d.target.id === devId) {
-                links.push(d);
-            }
-        });
-        return links;
-    }
 
     function removeLinkElement(d) {
         var idx = fs.find(d.key, network.links, 'key'),
@@ -475,8 +351,8 @@
     function removeDeviceElement(d) {
         var id = d.id;
         // first, remove associated hosts and links..
-        findAttachedHosts(id).forEach(removeHostElement);
-        findAttachedLinks(id).forEach(removeLinkElement);
+        tms.findAttachedHosts(id).forEach(removeHostElement);
+        tms.findAttachedLinks(id).forEach(removeLinkElement);
 
         // remove from lookup cache
         delete lu[id];
@@ -485,7 +361,7 @@
         network.nodes.splice(idx, 1);
 
         if (!network.nodes.length) {
-            xlink.showNoDevs(true);
+            uplink.showNoDevs(true);
         }
 
         // remove from SVG
@@ -502,11 +378,11 @@
         function updDev(d, show) {
             sus.makeVisible(d.el, show);
 
-            findAttachedLinks(d.id).forEach(function (link) {
+            tms.findAttachedLinks(d.id).forEach(function (link) {
                 b = show && ((link.type() !== 'hostLink') || showHosts);
                 sus.makeVisible(link.el, b);
             });
-            findAttachedHosts(d.id).forEach(function (host) {
+            tms.findAttachedHosts(d.id).forEach(function (host) {
                 b = show && showHosts;
                 sus.makeVisible(host.el, b);
             });
@@ -517,7 +393,7 @@
             updDev(dev, dev.online || showOffline);
         } else {
             // updating all offline devices
-            findDevices(true).forEach(function (d) {
+            tms.findDevices(true).forEach(function (d) {
                 updDev(d, showOffline);
             });
         }
@@ -532,12 +408,7 @@
         // attach the x, y, longitude, latitude...
         if (!clearPos) {
             ll = tms.lngLatFromCoord([d.x, d.y]);
-            metaUi = {
-                x: d.x,
-                y: d.y,
-                lng: ll[0],
-                lat: ll[1]
-            };
+            metaUi = {x: d.x, y: d.y, lng: ll[0], lat: ll[1]};
         }
         d.metaUi = metaUi;
         uplink.sendEvent('updateMeta', {
@@ -691,7 +562,7 @@
 
     function cycleDeviceLabels() {
         deviceLabelIndex = (deviceLabelIndex+1) % 3;
-        findDevices().forEach(function (d) {
+        tms.findDevices().forEach(function (d) {
             updateDeviceLabel(d);
         });
     }
@@ -1107,7 +978,7 @@
         },
         linkLabelAttr: {
             transform: function (d) {
-                var lnk = findLinkById(d.key);
+                var lnk = tms.findLinkById(d.key);
                 if (lnk) {
                     return transformLabel({
                         x1: lnk.source.x,
@@ -1223,6 +1094,15 @@
     // ==========================
     // Module definition
 
+    function mkModelApi(uplink) {
+        return {
+            projection: uplink.projection,
+            network: network,
+            restyleLinkElement: restyleLinkElement,
+            removeLinkElement: removeLinkElement
+        };
+    }
+
     angular.module('ovTopo')
     .factory('TopoForceService',
         ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
@@ -1241,7 +1121,7 @@
             icfg = is.iconConfig();
 
             // forceG is the SVG group to display the force layout in
-            // xlink is the cross-link api from the main topo source file
+            // uplink is the api from the main topo source file
             // dim is the initial dimensions of the SVG as [w,h]
             // opts are, well, optional :)
             function initForce(forceG, _uplink_, _dim_, opts) {
@@ -1250,10 +1130,7 @@
 
                 $log.debug('initForce().. dim = ' + dim);
 
-                tms.initModel({
-                    projection: uplink.projection,
-                    lookup: network.lookup
-                }, dim);
+                tms.initModel(mkModelApi(uplink), dim);
 
                 settings = angular.extend({}, defaultSettings, opts);
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topoModel.js b/web/gui/src/main/webapp/app/view/topo/topoModel.js
index 015fbdd..4754f60 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoModel.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoModel.js
@@ -26,6 +26,15 @@
     // injected refs
     var $log, fs, rnd, api;
 
+    // shorthand
+    var lu, rlk, nodes, links;
+
+    // api:
+    //   projection: func()
+    //   network {...}
+    //   restyleLinkElement: func(ldata)
+    //   removeLinkElement: func(ldata)
+
     var dim;    // dimensions of layout, as [w,h]
 
     // configuration 'constants'
@@ -95,7 +104,7 @@
         }
 
         function getDevice(cp) {
-            var d = api.lookup[cp.device];
+            var d = lu[cp.device];
             return d || rand();
         }
 
@@ -194,8 +203,8 @@
 
 
     function linkEndPoints(srcId, dstId) {
-        var srcNode = api.lookup[srcId],
-            dstNode = api.lookup[dstId],
+        var srcNode = lu[srcId],
+            dstNode = lu[dstId],
             sMiss = !srcNode ? missMsg('src', srcId) : '',
             dMiss = !dstNode ? missMsg('dst', dstId) : '';
 
@@ -218,6 +227,127 @@
         return '\n[' + what + '] "' + id + '" missing';
     }
 
+
+    function makeNodeKey(d, what) {
+        var port = what + 'Port';
+        return d[what] + '/' + d[port];
+    }
+
+    function makeLinkKey(d, flipped) {
+        var one = flipped ? makeNodeKey(d, 'dst') : makeNodeKey(d, 'src'),
+            two = flipped ? makeNodeKey(d, 'src') : makeNodeKey(d, 'dst');
+        return one + '-' + two;
+    }
+
+    function findLinkById(id) {
+        // check to see if this is a reverse lookup, else default to given id
+        var key = rlk[id] || id;
+        return key && lu[key];
+    }
+
+    function findLink(linkData, op) {
+        var key = makeLinkKey(linkData),
+            keyrev = makeLinkKey(linkData, 1),
+            link = lu[key],
+            linkRev = lu[keyrev],
+            result = {},
+            ldata = link || linkRev,
+            rawLink;
+
+        if (op === 'add') {
+            if (link) {
+                // trying to add a link that we already know about
+                result.ldata = link;
+                result.badLogic = 'addLink: link already added';
+
+            } else if (linkRev) {
+                // we found the reverse of the link to be added
+                result.ldata = linkRev;
+                if (linkRev.fromTarget) {
+                    result.badLogic = 'addLink: link already added';
+                }
+            }
+        } else if (op === 'update') {
+            if (!ldata) {
+                result.badLogic = 'updateLink: link not found';
+            } else {
+                rawLink = link ? ldata.fromSource : ldata.fromTarget;
+                result.updateWith = function (data) {
+                    angular.extend(rawLink, data);
+                    api.restyleLinkElement(ldata);
+                }
+            }
+        } else if (op === 'remove') {
+            if (!ldata) {
+                result.badLogic = 'removeLink: link not found';
+            } else {
+                rawLink = link ? ldata.fromSource : ldata.fromTarget;
+
+                if (!rawLink) {
+                    result.badLogic = 'removeLink: link not found';
+
+                } else {
+                    result.removeRawLink = function () {
+                        if (link) {
+                            // remove fromSource
+                            ldata.fromSource = null;
+                            if (ldata.fromTarget) {
+                                // promote target into source position
+                                ldata.fromSource = ldata.fromTarget;
+                                ldata.fromTarget = null;
+                                ldata.key = keyrev;
+                                delete lu[key];
+                                lu[keyrev] = ldata;
+                                delete rlk[keyrev];
+                            }
+                        } else {
+                            // remove fromTarget
+                            ldata.fromTarget = null;
+                            delete rlk[keyrev];
+                        }
+                        if (ldata.fromSource) {
+                            api.restyleLinkElement(ldata);
+                        } else {
+                            api.removeLinkElement(ldata);
+                        }
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    function findDevices(offlineOnly) {
+        var a = [];
+        nodes.forEach(function (d) {
+            if (d.class === 'device' && !(offlineOnly && d.online)) {
+                a.push(d);
+            }
+        });
+        return a;
+    }
+
+    function findAttachedHosts(devId) {
+        var hosts = [];
+        nodes.forEach(function (d) {
+            if (d.class === 'host' && d.cp.device === devId) {
+                hosts.push(d);
+            }
+        });
+        return hosts;
+    }
+
+    function findAttachedLinks(devId) {
+        var links = [];
+        links.forEach(function (d) {
+            if (d.source.id === devId || d.target.id === devId) {
+                links.push(d);
+            }
+        });
+        return links;
+    }
+
+
     // ==========================
     // Module definition
 
@@ -233,6 +363,10 @@
             function initModel(_api_, _dim_) {
                 api = _api_;
                 dim = _dim_;
+                lu = api.network.lookup;
+                rlk = api.network.revLinkToKey;
+                nodes = api.network.nodes;
+                links = api.network.links;
             }
 
             function newDim(_dim_) {
@@ -250,6 +384,11 @@
                 createLink: createLink,
                 coordFromLngLat: coordFromLngLat,
                 lngLatFromCoord: lngLatFromCoord,
+                findLink: findLink,
+                findLinkById: findLinkById,
+                findDevices: findDevices,
+                findAttachedHosts: findAttachedHosts,
+                findAttachedLinks: findAttachedLinks
             }
         }]);
 }());
diff --git a/web/gui/src/main/webapp/tests/app/view/topo/topoModel-spec.js b/web/gui/src/main/webapp/tests/app/view/topo/topoModel-spec.js
index a0d488b..67291f3 100644
--- a/web/gui/src/main/webapp/tests/app/view/topo/topoModel-spec.js
+++ b/web/gui/src/main/webapp/tests/app/view/topo/topoModel-spec.js
@@ -45,23 +45,22 @@
         return [xy[0] + 2000, xy[1] + 3000];
     };
 
-    // our test device lookup
-    var lu = {
-        dev1: {
+    // our test devices and hosts:
+    var dev1 = {
             'class': 'device',
             id: 'dev1',
             x: 17,
             y: 27,
             online: true
         },
-        dev2: {
+        dev2 = {
             'class': 'device',
             id: 'dev2',
             x: 18,
             y: 28,
             online: true
         },
-        host1: {
+        host1 = {
             'class': 'host',
             id: 'host1',
             x: 23,
@@ -72,7 +71,7 @@
             },
             ingress: 'dev1/7-host1'
         },
-        host2: {
+        host2 = {
             'class': 'host',
             id: 'host2',
             x: 24,
@@ -82,13 +81,20 @@
                 port: 0
             },
             ingress: 'dev0/0-host2'
-        }
-    };
+        };
+
 
     // our test api
     var api = {
         projection: function () { return mockProjection; },
-        lookup: lu
+        network: {
+            nodes: [dev1, dev2, host1, host2],
+            links: [],
+            lookup: {dev1: dev1, dev2: dev2, host1: host1, host2: host2},
+            revLinkToKey: {}
+        },
+        restyleLinkElement: function () {},
+        removeLinkElement: function () {}
     };
 
     // our test dimensions and well known locations..
@@ -204,7 +210,9 @@
             'initModel', 'newDim',
             'positionNode', 'createDeviceNode', 'createHostNode',
             'createHostLink', 'createLink',
-            'coordFromLngLat', 'lngLatFromCoord'
+            'coordFromLngLat', 'lngLatFromCoord',
+            'findLink', 'findLinkById', 'findDevices',
+            'findAttachedHosts', 'findAttachedLinks'
         ])).toBeTruthy();
     });
 
@@ -348,9 +356,9 @@
     // === unit tests for createHostLink()
 
     it('should create a basic host link', function () {
-        var link = tms.createHostLink(lu.host1);
-        expect(link.source).toEqual(lu.host1);
-        expect(link.target).toEqual(lu.dev1);
+        var link = tms.createHostLink(host1);
+        expect(link.source).toEqual(host1);
+        expect(link.target).toEqual(dev1);
         expect(link).toHaveEndPoints(host1Loc, dev1Loc);
         expect(link.key).toEqual('dev1/7-host1');
         expect(link.class).toEqual('link');
@@ -361,7 +369,7 @@
 
     it('should return null for failed endpoint lookup', function () {
         spyOn($log, 'error');
-        var link = tms.createHostLink(lu.host2);
+        var link = tms.createHostLink(host2);
         expect(link).toBeNull();
         expect($log.error).toHaveBeenCalledWith(
             'Node(s) not on map for link:\n[dst] "dev0" missing'
@@ -389,8 +397,8 @@
                 linkWidth: 1.5
             },
             link = tms.createLink(linkData);
-        expect(link.source).toEqual(lu.dev1);
-        expect(link.target).toEqual(lu.dev2);
+        expect(link.source).toEqual(dev1);
+        expect(link.target).toEqual(dev2);
         expect(link).toHaveEndPoints(dev1Loc, dev2Loc);
         expect(link.key).toEqual('baz');
         expect(link.class).toEqual('link');
@@ -400,4 +408,5 @@
         expect(link.linkWidth()).toEqual(1.5);
     });
 
+    // TODO: more unit tests for additional functions....
 });
diff --git a/web/gui/src/test/_karma/ev/simple/ev_17_removeDevice_08.json b/web/gui/src/test/_karma/ev/simple/ev_17_removeDevice_08.json
new file mode 100644
index 0000000..15e711d
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/simple/ev_17_removeDevice_08.json
@@ -0,0 +1,23 @@
+{
+  "event": "removeDevice",
+  "payload": {
+    "id": "of:0000ffffffff0008",
+    "type": "switch",
+    "online": false,
+    "master": "myInstA",
+    "location": {
+      "type": "latlng",
+      "lat": 37.7833,
+      "lng": -122.4167
+    },
+    "labels": [
+      "",
+      "sw-8",
+      "0000ffffffff0008"
+    ],
+    "metaUi": {
+      "x": 520,
+      "y": 350
+    }
+  }
+}