GUI -- Augmented sprite JSON definition.
- refactored topoSprite.js to handle the changes.

Change-Id: Ib0ed7dbacbc93777d8849bf82f52ad6ac974af2c
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.css b/web/gui/src/main/webapp/app/view/topo/topo.css
index 887c2fc..a254277 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -542,46 +542,60 @@
     fill: #eee;
 }
 
+/* ------------------------------------------------- */
 /* Sprite Layer */
 
+#ov-topo svg #topo-sprites use {
+    stroke-width: 1.0;
+}
 #ov-topo svg #topo-sprites text {
     text-anchor: middle;
     font-size: 10pt;
     font-style: italic;
 }
 
-.light #ov-topo svg #topo-sprites .sprite1 use {
-    stroke-width: 1.0;
-    stroke: goldenrod;
+.light #ov-topo svg #topo-sprites .gold1 use {
+    stroke: #da2;
     fill: none;
 }
-.dark #ov-topo svg #topo-sprites .sprite1 use {
-    stroke-width: 1.0;
+.dark #ov-topo svg #topo-sprites .gold1 use {
     stroke: #541;
     fill: none;
 }
-
-.light #ov-topo svg #topo-sprites .sprite1 text {
+.light #ov-topo svg #topo-sprites .gold1 text {
     fill: #eda;
 }
-.dark #ov-topo svg #topo-sprites .sprite1 text {
+.dark #ov-topo svg #topo-sprites .gold1 text {
     fill: #543;
 }
 
-.light #ov-topo svg #topo-sprites .sprite2 use {
+.light #ov-topo svg #topo-sprites .blue1 use {
     stroke: #bbd;
-    stroke-width: 1.0;
     fill: none;
 }
-.dark #ov-topo svg #topo-sprites .sprite2 use {
+.dark #ov-topo svg #topo-sprites .blue1 use {
     stroke: #445;
-    stroke-width: 1.0;
     fill: none;
 }
-
-.light #ov-topo svg #topo-sprites .sprite2 text {
+.light #ov-topo svg #topo-sprites .blue1 text {
     fill: #cce;
 }
-.dark #ov-topo svg #topo-sprites .sprite2 text {
+.dark #ov-topo svg #topo-sprites .blue1 text {
     fill: #446;
 }
+
+.light #ov-topo svg #topo-sprites .gray1 use {
+    stroke: #bbb;
+    fill: none;
+}
+.dark #ov-topo svg #topo-sprites .gray1 use {
+    stroke: #333;
+    fill: none;
+}
+.light #ov-topo svg #topo-sprites .gray1 text {
+    fill: #ccc;
+}
+.dark #ov-topo svg #topo-sprites .gray1 text {
+    fill: #444;
+}
+
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 f4d08f2..2233bb2 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -366,7 +366,7 @@
                 }
             );
             spriteG = zoomLayer.append ('g').attr('id', 'topo-sprites');
-            tspr.loadSprites(spriteG, $loc.search().sprites);
+            tspr.loadSprites(spriteG, defs, $loc.search().sprites);
 
             forceG = zoomLayer.append('g').attr('id', 'topo-force');
             tfs.initForce(svg, forceG, uplink, dim);
diff --git a/web/gui/src/main/webapp/app/view/topo/topoSprite.js b/web/gui/src/main/webapp/app/view/topo/topoSprite.js
index 1586a04..722feb0 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoSprite.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoSprite.js
@@ -16,47 +16,85 @@
 
 /*
  ONOS GUI -- Topology Sprite Module.
- Defines behavior for loading sprites.
+ Defines behavior for loading sprites into the sprite layer.
  */
 
 (function () {
     'use strict';
 
     // injected refs
-    var $log, $http, fs, sus, wss;
+    var $log, $http, fs, gs, sus, wss;
 
     var tssid = 'TopoSpriteService: ';
 
     // internal state
-    var spriteLayer;
+    var spriteLayer, defsElement;
 
-    function doSprite(def, item) {
-        var g;
+    function registerPathsAsGlyphs(paths) {
+        var custom = {},
+            ids = [];
 
-        function xfm(x, y, s) {
-            return sus.translate([x,y]) + sus.scale(s, s);
+        function mkd(d) {
+            return fs.isA(d) ? d.join('') : d;
         }
 
-        g = spriteLayer.append('g')
-            .classed(def['class'], true)
-            .attr('transform', xfm(item.x, item.y, def.scale));
+        if (paths) {
+            paths.forEach(function (path) {
+                var tag = 'spr_' + path.tag;
+                custom['_' + tag] = path.viewbox || '0 0 1000 1000';
+                custom[tag] = mkd(path.d);
+                ids.push(tag);
+            });
 
-        if (item.label) {
-            g.append('text')
-                .text(item.label)
-                .attr({
-                    x: def.width / 2,
-                    y: def.height * def.textyoff
-                });
+            gs.registerGlyphs(custom);
+            gs.loadDefs(defsElement, ids, true);
         }
+    }
+
+    function labAttr(def) {
+        var dim = def.dim || [1000,1000],
+            w = dim[0],
+            h = dim[1],
+            dy = def.labelyoff || 1;
+
+        return { x: w / 2, y: h * dy };
+    }
+
+    function doSprite(spr, def) {
+        var c = spr.class || 'gray1',
+            p = spr.pos || [0,0],
+            lab = spr.label,
+            dim = def.dim || [1000,1000],
+            w = dim[0],
+            h = dim[1],
+            use = def.glyph || 'spr_' + def.path,
+            g = spriteLayer.append('g')
+                .classed(c, true)
+                .attr('transform', sus.translate(p));
 
         g.append('use').attr({
-            width: def.width,
-            height: def.height,
-            'xlink:href': '#' + def.use
+            width: w,
+            height: h,
+            'xlink:href': '#' + use
         });
+
+        if (lab) {
+            g.append('text')
+                .text(lab)
+                .attr(labAttr(def));
+        }
     }
 
+    function doLabel(label) {
+        var c = label.class || 'gray1',
+            p = label.pos || [0,0];
+        spriteLayer.append('text')
+            .text(label.text)
+            .attr('transform', sus.translate(p))
+            .classed(c, true);
+    }
+
+
     // ==========================
     // event handlers
 
@@ -73,30 +111,49 @@
     //  data for the requested sprite definition.
     function inData(payload) {
         var data = payload.data,
-            name = data && data.defn_name,
-            desc = data && data.defn_desc,
+            name, desc, sprites, labels,
+            paths = {},
             defs = {};
 
         if (!data) {
             $log.warn(tssid + 'No sprite data loaded.')
             return;
         }
+        name = data.defn_name;
+        desc = data.defn_desc;
 
         $log.debug("Loading sprites...[" + name + "]", desc);
 
-        data.defn.forEach(function (d) {
-            defs[d.id] = d;
-        });
+        registerPathsAsGlyphs(data.paths);
 
-        data.load.forEach(function (item) {
-            doSprite(defs[item.id], item);
-        });
+        if (data.defn) {
+            data.defn.forEach(function (d) {
+                defs[d.id] = d;
+            });
+        }
+
+        // pull out the sprite and label items
+        if (data.load) {
+            sprites = data.load.sprites;
+            labels = data.load.labels;
+        }
+
+        if (sprites) {
+            sprites.forEach(function (spr) {
+               doSprite(spr, defs[spr.id]);
+            });
+        }
+
+        if (labels) {
+            labels.forEach(doLabel);
+        }
     }
 
 
-    function loadSprites(layer, defname) {
+    function loadSprites(layer, defsElem, defname) {
         var name = defname || 'sprites';
         spriteLayer = layer;
+        defsElement = defsElem;
 
         $log.info(tssid + 'Requesting sprite definition ['+name+']...');
 
@@ -109,12 +166,14 @@
 
     angular.module('ovTopo')
     .factory('TopoSpriteService',
-        ['$log', '$http', 'FnService', 'SvgUtilService', 'WebSocketService',
+        ['$log', '$http', 'FnService', 'GlyphService',
+            'SvgUtilService', 'WebSocketService',
 
-        function (_$log_, _$http_, _fs_, _sus_, _wss_) {
+        function (_$log_, _$http_, _fs_, _gs_, _sus_, _wss_) {
             $log = _$log_;
             $http = _$http_;
             fs = _fs_;
+            gs = _gs_;
             sus = _sus_;
             wss = _wss_;
 
diff --git a/web/gui/src/main/webapp/data/sprites/layout.json b/web/gui/src/main/webapp/data/sprites/layout.json
new file mode 100644
index 0000000..fac4768
--- /dev/null
+++ b/web/gui/src/main/webapp/data/sprites/layout.json
@@ -0,0 +1,122 @@
+{
+  "defn_name": "layout",
+  "defn_desc": "Sample Layout Sprite Data",
+
+  "_comment": [
+    "Sample sprite layout file, demonstrating user-defined outlines",
+    "(1) Register on the server with ...",
+    "    onos-upload-sprites localhost layout.json",
+    "(2) Load into topology view with ...",
+    "    http://localhost:8181/onos/ui/index.html#/topo?sprites=layout"
+  ],
+
+  "_comment_paths": [
+    "The 'paths' array contains custom path data.",
+    "Note that viewbox defaults to [0 0 1000 1000], which is the logical",
+    "coordinate space of the topology view."
+  ],
+  "paths": [
+    {
+      "tag": "border",
+      "d": "M0,0h1000v1000h-1000z",
+      "_comment": "bounds of viewbox 0 0 1000 1000"
+    },
+    {
+      "tag": "multi",
+      "d": [
+        "M500,500l-50,50v-200h100v200z",
+        "M600,400h200v50h-200z"
+      ],
+      "_comment": "shows path constructed from multiple strings"
+    },
+    {
+      "tag": "triangle",
+      "viewbox": "0 0 1 1",
+      "d": "M.5,.2l.3,.6,h-.6z",
+      "_comment": "defines its own viewbox"
+    },
+    {
+      "tag": "diamond",
+      "viewbox": "0 0 1 1",
+      "d": "M.2,.5l.3,-.3l.3,.3l-.3,.3z"
+    }
+  ],
+
+  "_comment_defn": [
+    "The 'defn' array contains sprite definitions that combine",
+    "path, dimensions, and label-offset into 'sprites' that can be",
+    "replicated (stamped) in different positions in the view.",
+    "",
+    "The 'glyph' property refers to glyphs registered with the UI.",
+    "Alternatively, the 'path' property refers to a custom path defined in",
+    "the path array above. The 'dim' property provides the [width,height]",
+    "bounds within which the glyph/path is drawn. The 'labelyoff' property",
+    "defines the Y-offset of the label as a percentage from the top of the",
+    "sprite; for example, 0.4 = 40%. The label is centered horizontally.",
+    "",
+    "Note that dimension (dim) defaults to [1000,1000] so that, by default,",
+    "there is a 1:1 scale mapping of custom paths to the topology view."
+  ],
+  "defn": [
+    {
+      "id": "border",
+      "path": "border"
+    },
+    {
+      "id": "multi",
+      "path": "multi"
+    },
+    {
+      "id": "small_tri",
+      "path": "triangle",
+      "dim":[80,80]
+    },
+    {
+      "id": "big_tri",
+      "path": "triangle",
+      "dim":[160,160]
+    },
+    {
+      "id": "subnet",
+      "glyph": "cloud",
+      "dim":[120,120],
+      "labelyoff": 0.4
+    },
+    {
+      "id": "subnet2",
+      "glyph": "cloud",
+      "dim":[200,200],
+      "labelyoff": 0.4
+    }
+  ],
+
+  "_comment_load": [
+    "The 'load' object contains sprites and labels to load into the view.",
+    "",
+    "Items in the sprite list associate sprites with a position,",
+    "style class, and optional label. Note that the coordinates of",
+    "the position define the top-left corner of the sprite.",
+    "Default 'pos' is [0,0]. Default 'class' is 'gray1'.",
+    "",
+    "Items in the label list associate labels with a position and",
+    "style class. Note that the text is centered on the x-coordinate."
+  ],
+  "load": {
+    "sprites": [
+      { "id": "border" },
+      { "id": "multi", "class": "gray1" },
+      { "id": "subnet", "pos":[-40,20], "label":"apples", "class": "blue1" },
+      { "id": "subnet", "pos":[400,40], "label":"bananas", "class": "blue1" },
+      { "id": "subnet", "pos":[840,60], "label":"cherries", "class": "blue1" },
+      { "id": "subnet2", "pos":[300,400], "class": "gray1" },
+      { "id": "small_tri", "pos":[10, 20] },
+      { "id": "small_tri", "pos":[110, 20] },
+      { "id": "small_tri", "pos":[210, 20] },
+      { "id": "small_tri", "pos":[310, 20] }
+    ],
+    "labels": [
+      { "pos":[500,940], "text":"Sample Layout", "class":"blue1" },
+      { "pos":[500,1000], "text":"Illustrating Sprites", "class":"gray1" }
+    ]
+  }
+}
diff --git a/web/gui/src/main/webapp/data/sprites/sprites.json b/web/gui/src/main/webapp/data/sprites/sample.json
similarity index 100%
rename from web/gui/src/main/webapp/data/sprites/sprites.json
rename to web/gui/src/main/webapp/data/sprites/sample.json