GUI -- Revamp of the Glyph Service to allow for custom viewboxes to be defined for registered glyphs/sprites.
- Also, initial sketch for externally injected sprite definition and placement.
- Added 'cloud' sprite data.

Change-Id: I1c38d50212a6d67e00e9b7c15427f6e0af40b539
diff --git a/web/gui/src/main/webapp/app/fw/svg/glyph.js b/web/gui/src/main/webapp/app/fw/svg/glyph.js
index ba5c08a..0612427 100644
--- a/web/gui/src/main/webapp/app/fw/svg/glyph.js
+++ b/web/gui/src/main/webapp/app/fw/svg/glyph.js
@@ -24,15 +24,13 @@
     var $log, fs, sus;
 
     // internal state
-    var glyphs = d3.map(),
-        msgGS = 'GlyphService.';
+    var glyphs = d3.map();
 
     // ----------------------------------------------------------------------
     // Base set of Glyphs...
 
-    var birdViewBox = '352 224 113 112',
-
-        birdData = {
+    var birdData = {
+            _bird: "352 224 113 112",
             bird: "M427.7,300.4 c-6.9,0.6-13.1,5-19.2,7.1c-18.1,6.2-33.9," +
             "9.1-56.5,4.7c24.6,17.2,36.6,13,63.7,0.1c-0.5,0.6-0.7,1.3-1.3," +
             "1.9c1.4-0.4,2.4-1.7,3.4-2.2c-0.4,0.7-0.9,1.5-1.4,1.9c2.2-0.6," +
@@ -47,9 +45,9 @@
             "C429.9,285.5,426.7,293.2,427.7,300.4z"
         },
 
-        glyphViewBox = '0 0 110 110',
+        glyphDataSet = {
+            _viewbox: "0 0 110 110",
 
-        glyphData = {
             unknown: "M35,40a5,5,0,0,1,5-5h30a5,5,0,0,1,5,5v30a5,5,0,0,1-5,5" +
             "h-30a5,5,0,0,1-5-5z",
 
@@ -288,9 +286,9 @@
             "L22,23.7z M97.9,46.5H77.2L88,23.7L97.9,46.5z"
         },
 
-        badgeViewBox = '0 0 10 10',
+        badgeDataSet = {
+            _viewbox: "0 0 10 10",
 
-        badgeData = {
             uiAttached: "M2,2.5a.5,.5,0,0,1,.5-.5h5a.5,.5,0,0,1,.5,.5v3" +
             "a.5,.5,0,0,1-.5,.5h-5a.5,.5,0,0,1-.5-.5zM2.5,2.8a.3,.3,0,0,1," +
             ".3-.3h4.4a.3,.3,0,0,1,.3,.3v2.4a.3,.3,0,0,1-.3,.3h-4.4" +
@@ -324,9 +322,70 @@
             play: "M2.5,2l5.5,3l-5.5,3z",
 
             stop: "M2.5,2.5h5v5h-5z"
+        },
+
+        spriteData = {
+            _cloud: '0 0 110 110',
+            cloud: "M37.6,79.5c-6.9,8.7-20.4,8.6-22.2-2.7" +
+            "M16.3,41.2c-0.8-13.9,19.4-19.2,23.5-7.8" +
+            "M38.9,30.9c5.1-9.4,15.1-8.5,16.9-1.3" +
+            "M54.4,32.9c4-12.9,14.8-9.6,18.6-3.8" +
+            "M95.8,58.5c10-4.1,11.7-17.8-0.9-19.8" +
+            "M18.1,76.4C5.6,80.3,3.8,66,13.8,61.5" +
+            "M16.2,62.4C2.1,58.4,3.5,36,16.8,36.6" +
+            "M93.6,74.7c10.2-2,10.7-14,5.8-18.3" +
+            "M71.1,79.3c11.2,7.6,24.6,6.4,22.1-11.7" +
+            "M36.4,76.8c3.4,13.3,35.4,11.6,36.1-1.4" +
+            "M70.4,31c11.8-10.4,26.2-5.2,24.7,10.1"
         };
 
     // ----------------------------------------------------------------------
+    // === Constants
+
+    var msgGS = 'GlyphService.',
+        rg = "registerGlyphs(): ",
+        rgs = "registerGlyphSet(): ";
+
+    // ----------------------------------------------------------------------
+
+    function warn(msg) {
+        $log.warn(msgGS + msg);
+    }
+
+    function addToMap(key, value, vbox, overwrite, dups) {
+        if (!overwrite && glyphs.get(key)) {
+            dups.push(key);
+        } else {
+            glyphs.set(key, {id: key, vb: vbox, d: value});
+        }
+    }
+
+    function reportDups(dups, which) {
+        var ok = (dups.length == 0),
+            msg = 'ID collision: ';
+
+        if (!ok) {
+            dups.forEach(function (id) {
+                warn(which + msg + '"' + id + '"');
+            });
+        }
+        return ok;
+    }
+
+    function reportMissVb(missing, which) {
+        var ok = (missing.length == 0),
+            msg = 'Missing viewbox property: ';
+
+        if (!ok) {
+            missing.forEach(function (vbk) {
+                warn(which + msg + '"' + vbk + '"');
+            });
+        }
+        return ok;
+    }
+
+    // ----------------------------------------------------------------------
+    // === API functions ===
 
     function clear() {
         // start with a fresh map
@@ -335,30 +394,46 @@
 
     function init() {
         clear();
-        register(birdViewBox, birdData);
-        register(glyphViewBox, glyphData);
-        register(badgeViewBox, badgeData);
+        registerGlyphs(birdData);
+        registerGlyphSet(glyphDataSet);
+        registerGlyphSet(badgeDataSet);
+        registerGlyphs(spriteData);
     }
 
-    function register(viewBox, data, overwrite) {
-        var dmap = d3.map(data),
-            dups = [],
-            ok;
+    function registerGlyphs(data, overwrite) {
+        var dups = [],
+            missvb = [];
 
-        dmap.forEach(function (key, value) {
-            if (!overwrite && glyphs.get(key)) {
-                dups.push(key);
-            } else {
-                glyphs.set(key, {id: key, vb: viewBox, d: value});
+        angular.forEach(data, function (value, key) {
+            var vbk = '_' + key,
+                vb = data[vbk];
+
+            if (key[0] !== '_') {
+                if (!vb) {
+                    missvb.push(vbk);
+                    return;
+                }
+                addToMap(key, value, vb, overwrite, dups);
             }
         });
-        ok = (dups.length == 0);
-        if (!ok) {
-            dups.forEach(function (id) {
-                $log.warn(msgGS + 'register(): ID collision: "'+id+'"');
-            });
+        return reportDups(dups, rg) && reportMissVb(missvb, rg);
+    }
+
+    function registerGlyphSet(data, overwrite) {
+        var dups = [],
+            vb = data._viewbox;
+
+        if (!vb) {
+            warn(rgs + 'no "_viewbox" property found');
+            return false;
         }
-        return ok;
+
+        angular.forEach(data, function (value, key) {
+            if (key[0] !== '_') {
+                addToMap(key, value, vb, overwrite, dups);
+            }
+        });
+        return reportDups(dups, rgs);
     }
 
     function ids() {
@@ -428,7 +503,8 @@
             return {
                 clear: clear,
                 init: init,
-                register: register,
+                registerGlyphs: registerGlyphs,
+                registerGlyphSet: registerGlyphSet,
                 ids: ids,
                 glyph: glyph,
                 loadDefs: loadDefs,
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index f40beb1..b16cb6d 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -247,6 +247,25 @@
             .attr('opacity', b ? 1 : 0);
     }
 
+    function addSprites() {
+        var g = zoomLayer.append ('g').attr('id', 'topo-sprites');
+
+        function cloud(g, x, y) {
+            g.append('use').attr({
+                width: 100,
+                height: 100,
+                'xlink:href': '#cloud',
+                transform: sus.translate([x, y]) + sus.scale(4,4)
+            }).style('stroke', 'goldenrod')
+                .style('fill', 'none')
+                .style('stroke-width', 1.0);
+        }
+
+        cloud(g, 0, 50);
+        cloud(g, 800, 40);
+        cloud(g, 400, 450);
+    }
+
     // --- User Preferemces ----------------------------------------------
 
     var prefsState = {};
@@ -354,6 +373,7 @@
                     toggleMap(prefsState.bg);
                 }
             );
+     //       addSprites();
 
             forceG = zoomLayer.append('g').attr('id', 'topo-force');
             tfs.initForce(svg, forceG, uplink, dim);
diff --git a/web/gui/src/main/webapp/data/ext/sprites.json b/web/gui/src/main/webapp/data/ext/sprites.json
new file mode 100644
index 0000000..5cf4109
--- /dev/null
+++ b/web/gui/src/main/webapp/data/ext/sprites.json
@@ -0,0 +1,44 @@
+{
+  "_comment": [
+    "configuration file for loading canned and/or custom sprites (and labels)",
+    "into the topology view. These appear above the map layer, but below",
+    "the nodes/links layer."
+  ],
+
+  "_comment_defn": "'defn' array contains custom sprite definitions",
+  "defn": [
+
+  ],
+
+  "_comment_defstyle": "'defstyle' defines default styles to apply",
+  "defstyle": {
+    "sprite": {
+      "stroke": "goldenrod",
+      "stroke-width": 1.0,
+      "fill": "none"
+    },
+    "text": {
+      "text-style": "italic",
+      "test-size": "20pt"
+    }
+  },
+
+  "_comment_load": [
+    "'load' array contains list of sprites/labels to load",
+    " note that 'copies' array defines [x,y] coords to position copies"
+  ],
+  "load": [
+    {
+      "id": "cloud",
+      "width": 100,
+      "height": 100,
+      "scale": 4.0,
+      "copies": [
+        [0, 50], [800, 40], [400, 450]
+      ],
+      "style": {
+        "stroke": "green"
+      }
+    }
+  ]
+}
diff --git a/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js b/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js
index 9b24e47..2f168ae 100644
--- a/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/svg/glyph-spec.js
@@ -20,7 +20,7 @@
 describe('factory: fw/svg/glyph.js', function() {
     var $log, fs, gs, d3Elem, svg;
 
-    var numBaseGlyphs = 35,
+    var numBaseGlyphs = 36,
         vbBird = '352 224 113 112',
         vbGlyph = '0 0 110 110',
         vbBadge = '0 0 10 10',
@@ -67,6 +67,8 @@
             play: 'M2.5,2l5.5,3',
             stop: 'M2.5,2.5h5',
 
+            cloud: 'M37.6,79.5c-6.9,8.7-20.4,8.6',
+
             // our test ones..
             triangle: 'M.5,.2',
             diamond: 'M.2,.5'
@@ -81,6 +83,9 @@
         badgeIds = [
             'uiAttached', 'checkMark', 'xMark', 'triangleUp', 'triangleDown',
             'plus', 'minus', 'play', 'stop'
+        ],
+        spriteIds = [
+            'cloud'
         ];
 
     beforeEach(module('onosUtil', 'onosSvg'));
@@ -106,8 +111,9 @@
 
     it('should define api functions', function () {
         expect(fs.areFunctions(gs, [
-            'clear', 'init', 'register', 'ids', 'glyph', 'loadDefs', 'addGlyph'
-        ])).toBeTruthy();
+            'clear', 'init', 'registerGlyphs', 'registerGlyphSet',
+            'ids', 'glyph', 'loadDefs', 'addGlyph'
+        ])).toBe(true);
     });
 
     it('should start with no glyphs loaded', function () {
@@ -131,7 +137,7 @@
             glyph = gs.glyph(id),
             prefix = prefixLookup[pfxId],
             plen = prefix.length;
-        expect(fs.contains(gs.ids(), id)).toBeTruthy();
+        expect(fs.contains(gs.ids(), id)).toBe(true);
         expect(glyph).toBeDefined();
         expect(glyph.id).toEqual(id);
         expect(glyph.vb).toEqual(vbox);
@@ -139,7 +145,8 @@
     }
 
     it('should be configured with the correct number of glyphs', function () {
-        expect(1 + glyphIds.length + badgeIds.length).toEqual(numBaseGlyphs);
+        var nGlyphs = 1 + glyphIds.length + badgeIds.length + spriteIds.length;
+        expect(nGlyphs).toEqual(numBaseGlyphs);
     });
 
     it('should load the bird glyph', function() {
@@ -161,29 +168,64 @@
         });
     });
 
+    it('should load the sprites', function () {
+        gs.init();
+        spriteIds.forEach(function (id) {
+            verifyGlyphLoadedInCache(id, vbGlyph);
+        });
+    });
+
 
     // define some glyphs that we want to install
 
     var testVbox = '0 0 1 1',
+        triVbox = '0 0 12 12',
+        diaVbox = '0 0 15 15',
         dTriangle = 'M.5,.2l.3,.6,h-.6z',
         dDiamond = 'M.2,.5l.3,-.3l.3,.3l-.3,.3z',
         newGlyphs = {
+            _viewbox: testVbox,
             triangle: dTriangle,
             diamond: dDiamond
         },
         dupGlyphs = {
+            _viewbox: testVbox,
             router: dTriangle,
             switch: dDiamond
         },
-        idCollision = 'GlyphService.register(): ID collision: ';
+        altNewGlyphs = {
+            _triangle: triVbox,
+            triangle: dTriangle,
+            _diamond: diaVbox,
+            diamond: dDiamond
+        },
+        altDupGlyphs = {
+            _router: triVbox,
+            router: dTriangle,
+            _switch: diaVbox,
+            switch: dDiamond
+        },
+        badGlyphSet = {
+            triangle: dTriangle,
+            diamond: dDiamond
+        },
+        warnMsg = 'GlyphService.registerGlyphs(): ',
+        warnMsgSet = 'GlyphService.registerGlyphSet(): ',
+        idCollision = warnMsg + 'ID collision: ',
+        idCollisionSet = warnMsgSet + 'ID collision: ',
+        missVbSet = warnMsgSet + 'no "_viewbox" property found',
+        missVbCustom = warnMsg + 'Missing viewbox property: ',
+        missVbTri = missVbCustom + '"_triangle"',
+        missVbDia = missVbCustom + '"_diamond"';
 
-    it('should install new glyphs', function () {
+
+    it('should install new glyphs as a glyph-set', function () {
         gs.init();
         expect(gs.ids().length).toEqual(numBaseGlyphs);
         spyOn($log, 'warn');
 
-        var ok = gs.register(testVbox, newGlyphs);
-        expect(ok).toBeTruthy();
+        var ok = gs.registerGlyphSet(newGlyphs);
+        expect(ok).toBe(true);
         expect($log.warn).not.toHaveBeenCalled();
 
         expect(gs.ids().length).toEqual(numBaseGlyphs + 2);
@@ -191,13 +233,69 @@
         verifyGlyphLoadedInCache('diamond', testVbox);
     });
 
+    it('should not overwrite glyphs (via glyph-set) with dup IDs', function () {
+        gs.init();
+        expect(gs.ids().length).toEqual(numBaseGlyphs);
+        spyOn($log, 'warn');
+
+        var ok = gs.registerGlyphSet(dupGlyphs);
+        expect(ok).toBe(false);
+        expect($log.warn).toHaveBeenCalledWith(idCollisionSet + '"switch"');
+        expect($log.warn).toHaveBeenCalledWith(idCollisionSet + '"router"');
+
+        expect(gs.ids().length).toEqual(numBaseGlyphs);
+        // verify original glyphs still exist...
+        verifyGlyphLoadedInCache('router', vbGlyph);
+        verifyGlyphLoadedInCache('switch', vbGlyph);
+    });
+
+    it('should replace glyphs (via glyph-set) if asked nicely', function () {
+        gs.init();
+        expect(gs.ids().length).toEqual(numBaseGlyphs);
+        spyOn($log, 'warn');
+
+        var ok = gs.registerGlyphSet(dupGlyphs, true);
+        expect(ok).toBe(true);
+        expect($log.warn).not.toHaveBeenCalled();
+
+        expect(gs.ids().length).toEqual(numBaseGlyphs);
+        // verify glyphs have been overwritten...
+        verifyGlyphLoadedInCache('router', testVbox, 'triangle');
+        verifyGlyphLoadedInCache('switch', testVbox, 'diamond');
+    });
+
+    it ('should complain if missing _viewbox in a glyph-set', function () {
+        gs.init();
+        expect(gs.ids().length).toEqual(numBaseGlyphs);
+        spyOn($log, 'warn');
+
+        var ok = gs.registerGlyphSet(badGlyphSet);
+        expect(ok).toBe(false);
+        expect($log.warn).toHaveBeenCalledWith(missVbSet);
+        expect(gs.ids().length).toEqual(numBaseGlyphs);
+    });
+
+    it('should install new glyphs', function () {
+        gs.init();
+        expect(gs.ids().length).toEqual(numBaseGlyphs);
+        spyOn($log, 'warn');
+
+        var ok = gs.registerGlyphs(altNewGlyphs);
+        expect(ok).toBe(true);
+        expect($log.warn).not.toHaveBeenCalled();
+
+        expect(gs.ids().length).toEqual(numBaseGlyphs + 2);
+        verifyGlyphLoadedInCache('triangle', triVbox);
+        verifyGlyphLoadedInCache('diamond', diaVbox);
+    });
+
     it('should not overwrite glyphs with dup IDs', function () {
         gs.init();
         expect(gs.ids().length).toEqual(numBaseGlyphs);
         spyOn($log, 'warn');
 
-        var ok = gs.register(testVbox, dupGlyphs);
-        expect(ok).toBeFalsy();
+        var ok = gs.registerGlyphs(altDupGlyphs);
+        expect(ok).toBe(false);
         expect($log.warn).toHaveBeenCalledWith(idCollision + '"switch"');
         expect($log.warn).toHaveBeenCalledWith(idCollision + '"router"');
 
@@ -212,14 +310,26 @@
         expect(gs.ids().length).toEqual(numBaseGlyphs);
         spyOn($log, 'warn');
 
-        var ok = gs.register(testVbox, dupGlyphs, true);
-        expect(ok).toBeTruthy();
+        var ok = gs.registerGlyphs(altDupGlyphs, true);
+        expect(ok).toBe(true);
         expect($log.warn).not.toHaveBeenCalled();
 
         expect(gs.ids().length).toEqual(numBaseGlyphs);
         // verify glyphs have been overwritten...
-        verifyGlyphLoadedInCache('router', testVbox, 'triangle');
-        verifyGlyphLoadedInCache('switch', testVbox, 'diamond');
+        verifyGlyphLoadedInCache('router', triVbox, 'triangle');
+        verifyGlyphLoadedInCache('switch', diaVbox, 'diamond');
+    });
+
+    it ('should complain if missing custom viewbox', function () {
+        gs.init();
+        expect(gs.ids().length).toEqual(numBaseGlyphs);
+        spyOn($log, 'warn');
+
+        var ok = gs.registerGlyphs(badGlyphSet);
+        expect(ok).toBe(false);
+        expect($log.warn).toHaveBeenCalledWith(missVbTri);
+        expect($log.warn).toHaveBeenCalledWith(missVbDia);
+        expect(gs.ids().length).toEqual(numBaseGlyphs);
     });
 
     function verifyPathPrefix(elem, prefix) {
@@ -245,7 +355,7 @@
 
     it('should load custom glyphs into the DOM', function () {
         gs.init();
-        gs.register(testVbox, newGlyphs);
+        gs.registerGlyphSet(newGlyphs);
         gs.loadDefs(d3Elem);
         expect(d3Elem.selectAll('symbol').size()).toEqual(numBaseGlyphs + 2);
         verifyLoadedInDom('diamond', testVbox);