GUI -- Completed implementation of Instance events (add, update, remove)
- fixed instance color selection (using cat7() function)
- miscellaneous additions to utility functions.
- etc. and so on...

Change-Id: I61895489ccc60fa17beda9e920e65742e0f2c526
diff --git a/web/gui/src/main/webapp/app/fw/svg/svgUtil.js b/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
index 735733e..77c70a6 100644
--- a/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
+++ b/web/gui/src/main/webapp/app/fw/svg/svgUtil.js
@@ -132,8 +132,97 @@
                 $log.warn('SvgUtilService: loadGlow -- To Be Implemented');
             }
 
+            // --- Ordinal scales for 7 values.
+            // TODO: tune colors for light and dark themes
+            // Note: These colors look good on the white background. Still, need to tune for dark.
+
+            //               blue       brown      brick red  sea green  purple     dark teal  lime
+            var lightNorm = ['#3E5780', '#78533B', '#CB4D28', '#018D61', '#8A2979', '#006D73', '#56AF00'],
+                lightMute = ['#A8B8CC', '#CCB3A8', '#FFC2BD', '#96D6BF', '#D19FCE', '#8FCCCA', '#CAEAA4'],
+
+                darkNorm  = ['#3E5780', '#78533B', '#CB4D28', '#018D61', '#8A2979', '#006D73', '#56AF00'],
+                darkMute  = ['#A8B8CC', '#CCB3A8', '#FFC2BD', '#96D6BF', '#D19FCE', '#8FCCCA', '#CAEAA4'];
+
+            var colors= {
+                light: {
+                    norm: d3.scale.ordinal().range(lightNorm),
+                    mute: d3.scale.ordinal().range(lightMute)
+                },
+                dark: {
+                    norm: d3.scale.ordinal().range(darkNorm),
+                    mute: d3.scale.ordinal().range(darkMute)
+                }
+            };
+
             function cat7() {
-                $log.warn('SvgUtilService: cat7 -- To Be Implemented');
+                var tcid = 'd3utilTestCard';
+
+                function getColor(id, muted, theme) {
+                    // NOTE: since we are lazily assigning domain ids, we need to
+                    //       get the color from all 4 scales, to keep the domains
+                    //       in sync.
+                    var ln = colors.light.norm(id),
+                        lm = colors.light.mute(id),
+                        dn = colors.dark.norm(id),
+                        dm = colors.dark.mute(id);
+                    if (theme === 'dark') {
+                        return muted ? dm : dn;
+                    } else {
+                        return muted ? lm : ln;
+                    }
+                }
+
+                function testCard(svg) {
+                    var g = svg.select('g#' + tcid),
+                        dom = d3.range(7),
+                        k, muted, theme, what;
+
+                    if (!g.empty()) {
+                        g.remove();
+
+                    } else {
+                        g = svg.append('g')
+                            .attr('id', tcid)
+                            .attr('transform', 'scale(4)translate(20,20)');
+
+                        for (k=0; k<4; k++) {
+                            muted = k%2;
+                            what = muted ? ' muted' : ' normal';
+                            theme = k < 2 ? 'light' : 'dark';
+                            dom.forEach(function (id, i) {
+                                var x = i * 20,
+                                    y = k * 20,
+                                    f = get(id, muted, theme);
+                                g.append('circle').attr({
+                                    cx: x,
+                                    cy: y,
+                                    r: 5,
+                                    fill: f
+                                });
+                            });
+                            g.append('rect').attr({
+                                x: 140,
+                                y: k * 20 - 5,
+                                width: 32,
+                                height: 10,
+                                rx: 2,
+                                fill: '#888'
+                            });
+                            g.append('text').text(theme + what)
+                                .attr({
+                                    x: 142,
+                                    y: k * 20 + 2,
+                                    fill: 'white'
+                                })
+                                .style('font-size', '4pt');
+                        }
+                    }
+                }
+
+                return {
+                    testCard: testCard,
+                    getColor: getColor
+                };
             }
 
             function translate(x, y) {
diff --git a/web/gui/src/main/webapp/app/fw/util/fn.js b/web/gui/src/main/webapp/app/fw/util/fn.js
index 932a7c6..690c8b2 100644
--- a/web/gui/src/main/webapp/app/fw/util/fn.js
+++ b/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -45,7 +45,38 @@
 
     // Returns true if all names in the array are defined as functions
     // on the given api object; false otherwise.
+    // Also returns false if there are properties on the api that are NOT
+    //  listed in the array of names.
     function areFunctions(api, fnNames) {
+        var fnLookup = {},
+            extraFound = false;
+
+        if (!isA(fnNames)) {
+            return false;
+        }
+        var n = fnNames.length,
+            i, name;
+        for (i=0; i<n; i++) {
+            name = fnNames[i];
+            if (!isF(api[name])) {
+                return false;
+            }
+            fnLookup[name] = true;
+        }
+
+        // check for properties on the API that are not listed in the array,
+        angular.forEach(api, function (value, key) {
+            if (!fnLookup[key]) {
+                extraFound = true;
+            }
+        });
+        return !extraFound;
+    }
+
+    // Returns true if all names in the array are defined as functions
+    // on the given api object; false otherwise. This is a non-strict version
+    // that does not care about other properties on the api.
+    function areFunctionsNonStrict(api, fnNames) {
         if (!isA(fnNames)) {
             return false;
         }
@@ -71,6 +102,21 @@
         };
     }
 
+    // search through an array of objects, looking for the one with the
+    // tagged property matching the given key. tag defaults to 'id'.
+    // returns the index of the matching object, or -1 for no match.
+    function find(key, array, tag) {
+        var _tag = tag || 'id',
+            idx, n, d;
+        for (idx = 0, n = array.length; idx < n; idx++) {
+            d = array[idx];
+            if (d[_tag] === key) {
+                return idx;
+            }
+        }
+        return -1;
+    }
+
     angular.module('onosUtil')
         .factory('FnService', ['$window', function (_$window_) {
             $window = _$window_;
@@ -82,7 +128,9 @@
                 isO: isO,
                 contains: contains,
                 areFunctions: areFunctions,
-                windowSize: windowSize
+                areFunctionsNonStrict: areFunctionsNonStrict,
+                windowSize: windowSize,
+                find: find
             };
     }]);
 
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 fcff4e4..b4b5c74 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoEvent.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoEvent.js
@@ -30,7 +30,9 @@
 
     var evHandler = {
         showSummary: showSummary,
-        addInstance: addInstance
+        addInstance: addInstance,
+        updateInstance: updateInstance,
+        removeInstance: removeInstance
         // TODO: implement remaining handlers..
 
     };
@@ -51,6 +53,16 @@
         tis.addInstance(ev.payload);
     }
 
+    function updateInstance(ev) {
+        $log.debug('  **** Update Instance **** ', ev.payload);
+        tis.updateInstance(ev.payload);
+    }
+
+    function removeInstance(ev) {
+        $log.debug('  **** Remove Instance **** ', ev.payload);
+        tis.removeInstance(ev.payload);
+    }
+
     // ==========================
 
     var dispatcher = {
diff --git a/web/gui/src/main/webapp/app/view/topo/topoInst.js b/web/gui/src/main/webapp/app/view/topo/topoInst.js
index 12e47c6..7bd74f9 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoInst.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoInst.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // injected refs
-    var $log, ps, sus, gs;
+    var $log, ps, sus, gs, ts, fs;
 
     // configuration
     var instCfg = {
@@ -79,6 +79,23 @@
         }
     }
 
+    function removeInstance(data) {
+        var id = data.id,
+            d = onosInstances[id];
+        if (d) {
+            var idx = fs.find(id, onosOrder);
+            if (idx >= 0) {
+                onosOrder.splice(idx, 1);
+            }
+            delete onosInstances[id];
+            updateInstances();
+        } else {
+            logicError('removeInstance lookup fail. ID = "' + id + '"');
+        }
+    }
+
+    // ==========================
+
     function computeDim(self) {
         var css = window.getComputedStyle(self);
         return {
@@ -143,9 +160,7 @@
     }
 
     function instColor(id, online) {
-        // TODO: fix this..
-        //return cat7.get(id, !online, network.view.getTheme());
-        return '#3E5780';
+        return sus.cat7().getColor(id, !online, ts.theme());
     }
 
     // ==============================
@@ -288,17 +303,22 @@
     angular.module('ovTopo')
     .factory('TopoInstService',
         ['$log', 'PanelService', 'SvgUtilService', 'GlyphService',
+            'ThemeService', 'FnService',
 
-        function (_$log_, _ps_, _sus_, _gs_) {
+        function (_$log_, _ps_, _sus_, _gs_, _ts_, _fs_) {
             $log = _$log_;
             ps = _ps_;
             sus = _sus_;
             gs = _gs_;
+            ts = _ts_;
+            fs = _fs_;
 
             return {
                 initInst: initInst,
                 destroyInst: destroyInst,
-                addInstance: addInstance
+                addInstance: addInstance,
+                updateInstance: updateInstance,
+                removeInstance: removeInstance
             };
         }]);
 }());