First pass at svg icons and dark theme for topo
Have not addressed top bar etc

Change-Id: I0cc47a1f500bd9d8589eeaf8042f21ec4c8e6cbe
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 66366c7..f818e2f 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -70,23 +70,17 @@
             }
         },
         topo: {
-            linkBaseColor: '#666',
             linkInColor: '#66f',
-            linkInWidth: 14,
             linkOutColor: '#f00',
+            linkInWidth: 14,
             linkOutWidth: 30
         },
-        icons: {
-            w: 28,
-            h: 28,
-            xoff: -12,
-            yoff: -8
+        map: {
+            strokeWidth: 1
         },
-        iconUrl: {
-            device: 'img/device.png',
-            host: 'img/host.png',
-            pkt: 'img/pkt.png',
-            opt: 'img/opt.png'
+        icons: {
+            w: 100,
+            h: 100
         },
         force: {
             note_for_links: 'link.type is used to differentiate',
@@ -532,8 +526,7 @@
         }
         el.transition()
             .duration(1000)
-            .attr('stroke-width', linkScale(lw))
-            .attr('stroke', config.topo.linkBaseColor);
+            .attr('stroke-width', linkScale(lw));
     }
 
     // ==============================
@@ -1234,8 +1227,8 @@
         $.extend(node, xy);
     }
 
-    function iconUrl(d) {
-        return 'img/' + d.type + '.png';
+    function getIconUrl(d) {
+        return 'icons.svg#' + d.type;
     }
 
     // returns the newly computed bounding box of the rectangle
@@ -1277,6 +1270,29 @@
         return (label && label.trim()) ? label : '.';
     }
 
+    function updateDeviceIconAppearance(node, box, animate) {
+        var u = node.select('use');
+        var ubbox = u.node().getBBox();
+
+        var xoff = -ubbox.width/2 - box.width/2 - 4;
+        var yoff = -ubbox.height;
+        var iconTransform = 'translate(' + xoff + ',' + yoff + ')';
+        if (animate) {
+            node.select('use')
+                .transition()
+                .attr('transform', iconTransform);
+        } else {
+            node.select('use')
+                .attr('transform', iconTransform);
+        }
+
+        var computedStyle = window.getComputedStyle(node.node());
+        u.attr({
+            fill: computedStyle.fill,
+            color: computedStyle.color
+        });
+    }
+
     function updateDeviceLabel(d) {
         var label = niceLabel(deviceLabel(d)),
             node = d.el,
@@ -1294,10 +1310,7 @@
             .transition()
             .attr(box);
 
-        node.select('image')
-            .transition()
-            .attr('x', box.x + config.icons.xoff)
-            .attr('y', box.y + config.icons.yoff);
+        updateDeviceIconAppearance(node, box, true);
     }
 
     function updateHostLabel(d) {
@@ -1339,18 +1352,6 @@
         }
     }
 
-    function addHostIcon(node, radius, iconId) {
-        var dim = radius * 1.5,
-            xlate = -dim / 2;
-
-        node.append('use')
-            .classed('glyph', true)
-            .attr('transform', translate(xlate,xlate))
-            .attr('xlink:href', '#' + iconId)
-            .attr('width', dim)
-            .attr('height', dim);
-    }
-
     function updateNodes() {
         node = nodeG.selectAll('.node')
             .data(network.nodes, function (d) { return d.id; });
@@ -1377,7 +1378,7 @@
         // augment device nodes...
         entering.filter('.device').each(function (d) {
             var node = d3.select(this),
-                icon = iconUrl(d),
+                iconUrl = getIconUrl(d),
                 label = niceLabel(deviceLabel(d)),
                 box;
 
@@ -1399,18 +1400,17 @@
             node.select('rect')
                 .attr(box);
 
-            if (icon) {
-                var cfg = config.icons;
-                node.append('svg:image')
+            if (iconUrl) {
+                node.append('svg:use')
                     .attr({
-                        x: box.x + config.icons.xoff,
-                        y: box.y + config.icons.yoff,
-                        width: cfg.w,
-                        height: cfg.h,
-                        'xlink:href': icon
+                        'xlink:href': iconUrl,
+                        width: config.icons.w,
+                        height: config.icons.h
                     });
             }
 
+            updateDeviceIconAppearance(node, box, false);
+
             // debug function to show the modelled x,y coordinates of nodes...
             if (debug('showNodeXY')) {
                 node.select('rect').attr('fill-opacity', 0.5);
@@ -1424,36 +1424,43 @@
             }
         });
 
-        // TODO: better place for this configuration state
-        var defaultHostRadius = 9,
-            hostRadius = {
-                bgpSpeaker: 20
-            },
-            hostIcon = {
-                bgpSpeaker: 'bullhorn'
-            };
-
-
         // augment host nodes...
         entering.filter('.host').each(function (d) {
             var node = d3.select(this),
-                r = hostRadius[d.type] || defaultHostRadius,
-                textDy = r + 10,
-                icon = hostIcon[d.type];
+                iconUrl = getIconUrl(d);
 
             // provide ref to element from backing data....
             d.el = node;
 
-            node.append('circle')
-                .attr('r', r);
+            // var box = node.append('circle')
+            //     .attr('r', r).node().getBBox();
 
-            if (icon) {
-                addHostIcon(node, r, icon);
+            var textYOff = 0;
+            var textXOff = 0;
+            if (iconUrl) {
+                var computedStyle = window.getComputedStyle(node.node());
+                var u = node.append('svg:use')
+                    .attr({
+                        'xlink:href': iconUrl,
+                        width: config.icons.w,
+                        height: config.icons.h,
+                        fill: computedStyle.fill,
+                        color: computedStyle.color
+                    });
+
+                var ubbox = node.select('use').node().getBBox();
+                u.attr('y', -ubbox.height/2);
+                textYOff = ubbox.height/2 + 4; // pad by 4 pixels
+                textXOff = ubbox.width/2;
             }
 
+
+
             node.append('text')
                 .text(hostLabel)
-                .attr('dy', textDy)
+                .attr('alignment-baseline', 'text-before-edge')
+                .attr('x', textXOff)
+                .attr('y', textYOff)
                 .attr('text-anchor', 'middle');
 
             // debug function to show the modelled x,y coordinates of nodes...
@@ -1492,9 +1499,9 @@
                 .style('opacity', 0);
             // note, leave <g>.remove to remove this element
 
-            node.select('circle')
-                .style('stroke-fill', '#555')
-                .style('fill', '#888')
+            node.select('use')
+                .style('color', '#aaa')
+                .style('fill', '#000')
                 .style('opacity', 0.5)
                 .transition()
                 .duration(1500)
@@ -1817,7 +1824,7 @@
     function zoomPan(scale, translate) {
         zoomPanContainer.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
         // keep the map lines constant width while zooming
-        bgImg.style("stroke-width", 2.0 / scale + "px");
+        bgImg.attr("stroke-width", config.map.strokeWidth / scale + "px");
     }
 
     function resetZoomPan() {
@@ -1934,6 +1941,27 @@
         gly.defBullhorn(defs);
     }
 
+    // create references to bring these into cache so that getBBox() works when they
+    // are inserted later
+    function preloadIcons(svg) {
+        var icons = [
+            "router",
+            "switch",
+            "roadm",
+            "endstation",
+            "bgpSpeaker"
+        ];
+
+        var g = svg.append('g');
+        for (var icon in icons) {
+        g.append('use')
+                    .attr({
+                        'xlink:href': 'icons.svg#' + icon
+                    });
+        }
+        g.style('visibility', 'hidden');
+    }
+
     // ==============================
     // View life-cycle callbacks
 
@@ -1950,6 +1978,7 @@
         setSize(svg, view);
 
         loadGlyphs(svg);
+        preloadIcons(svg);
 
         zoomPanContainer = svg.append('g').attr('id', 'zoomPanContainer');
         setupZoomPan();
@@ -2046,7 +2075,7 @@
                     width: config.birdDim,
                     height: config.birdDim,
                     fill: '#111'
-                })
+                });
     }
 
     function para(sel, text) {
@@ -2161,11 +2190,13 @@
             .translate(t);
 
         bgImg = zoomPanContainer.insert("g", '#topo-G');
-        bgImg.attr('id', 'map').selectAll('path')
-            .data(topoData.features)
-            .enter()
-            .append('path')
-            .attr('d', path);
+        // pointer-events: none so that browser select tools don't pick up the map svg
+        bgImg.attr('id', 'map').attr('stroke-width', config.map.strokeWidth + 'px').style('pointer-events', 'none')
+            .selectAll('path')
+                .data(topoData.features)
+                .enter()
+                .append('path')
+                .attr('d', path);
     }
 
     function resize(view, ctx, flags) {