GUI -- Implemented ZoomService, with unit tests.
- Added zoomer to topo.js; we are at least generating the events.
- Added GlyphService.clear()

Change-Id: I5400e52b58ee584866d8ffbb20d5bde70b336985
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 8129c16..621436c 100644
--- a/web/gui/src/main/webapp/app/fw/svg/glyph.js
+++ b/web/gui/src/main/webapp/app/fw/svg/glyph.js
@@ -124,9 +124,13 @@
         .factory('GlyphService', ['$log', function (_$log_) {
             $log = _$log_;
 
-            function init() {
+            function clear() {
                 // start with a fresh map
                 glyphs = d3.map();
+            }
+
+            function init() {
+                clear();
                 register(birdViewBox, birdData);
                 register(glyphViewBox, glyphData);
                 register(badgeViewBox, badgeData);
@@ -175,6 +179,7 @@
             }
 
             return {
+                clear: clear,
                 init: init,
                 register: register,
                 ids: ids,
diff --git a/web/gui/src/main/webapp/app/fw/svg/zoom.js b/web/gui/src/main/webapp/app/fw/svg/zoom.js
index 2dc5236..09c8455 100644
--- a/web/gui/src/main/webapp/app/fw/svg/zoom.js
+++ b/web/gui/src/main/webapp/app/fw/svg/zoom.js
@@ -22,14 +22,112 @@
 (function () {
     'use strict';
 
+    // configuration
+    var defaultSettings = {
+        zoomMin: 0.25,
+        zoomMax: 10,
+        zoomEnabled: function (ev) { return true; },
+        zoomCallback: function () {}
+    };
+
+    // injected references to services
     var $log;
 
     angular.module('onosSvg')
-        .factory('ZoomService', ['$log', function (_$log_) {
+        .factory('ZoomService', ['$log',
+
+        function (_$log_) {
             $log = _$log_;
 
+/*
+            NOTE: opts is an object:
+            {
+                svg: svg,                       D3 selection of <svg> element
+                zoomLayer: zoomLayer,           D3 selection of <g> element
+                zoomEnabled: function (ev) { ... },
+                zoomCallback: function () { ... }
+            }
+
+            where:
+                * svg and zoomLayer should be D3 selections of DOM elements.
+                    * zoomLayer <g> is a child of <svg> element.
+                * zoomEnabled is an optional predicate based on D3 event.
+                    * default is always enabled.
+                * zoomCallback is an optional callback invoked each time we pan/zoom.
+                    * default is do nothing.
+
+            Optionally, zoomMin and zoomMax also can be defined.
+            These default to 0.25 and 10 respectively.
+*/
+            function createZoomer(opts) {
+                var cz = 'ZoomService.createZoomer(): ',
+                    d3s = ' (D3 selection) property defined',
+                    settings = $.extend({}, defaultSettings, opts),
+                    zoom = d3.behavior.zoom()
+                        .translate([0, 0])
+                        .scale(1)
+                        .scaleExtent([settings.zoomMin, settings.zoomMax])
+                        .on('zoom', zoomed),
+                    fail = false,
+                    zoomer;
+
+                if (!settings.svg) {
+                    $log.error(cz + 'No "svg" (svg tag)' + d3s);
+                    fail = true;
+                }
+                if (!settings.zoomLayer) {
+                    $log.error(cz + 'No "zoomLayer" (g tag)' + d3s);
+                    fail = true;
+                }
+
+                if (fail) {
+                    return null;
+                }
+
+                // zoom events from mouse gestures...
+                function zoomed() {
+                    var ev = d3.event.sourceEvent;
+                    if (settings.zoomEnabled(ev)) {
+                        adjustZoomLayer(d3.event.translate, d3.event.scale);
+                    }
+                }
+
+                function adjustZoomLayer(translate, scale) {
+                    settings.zoomLayer.attr('transform',
+                        'translate(' + translate + ')scale(' + scale + ')');
+                    settings.zoomCallback();
+                }
+
+                zoomer = {
+                    panZoom: function (translate, scale) {
+                        zoom.translate(translate).scale(scale);
+                        adjustZoomLayer(translate, scale);
+                    },
+
+                    reset: function () {
+                        zoomer.panZoom([0,0], 1);
+                    },
+
+                    translate: function () {
+                        return zoom.translate();
+                    },
+
+                    scale: function () {
+                        return zoom.scale();
+                    },
+
+                    scaleExtent: function () {
+                        return zoom.scaleExtent();
+                    }
+                };
+
+                // apply the zoom behavior to the SVG element
+                settings.svg && settings.svg.call(zoom);
+                return zoomer;
+            }
+
             return {
-                tbd: function () {}
+                createZoomer: createZoomer
             };
         }]);
 
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 adf7d2a..603a5a1 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -29,19 +29,22 @@
     ];
 
     // references to injected services etc.
-    var $log, ks, gs;
+    var $log, ks, zs, gs;
 
     // DOM elements
-    var defs;
+    var svg, defs;
 
     // Internal state
-    // ...
+    var zoomer;
 
     // Note: "exported" state should be properties on 'self' variable
 
+    // --- Short Cut Keys ------------------------------------------------
+
     var keyBindings = {
-        W: [logWarning, 'log a warning'],
-        E: [logError, 'log an error']
+        W: [logWarning, '(temp) log a warning'],
+        E: [logError, '(temp) log an error'],
+        R: [resetZoom, 'Reset pan / zoom']
     };
 
     // -----------------
@@ -54,32 +57,72 @@
     }
     // -----------------
 
+    function resetZoom() {
+        zoomer.reset();
+    }
+
     function setUpKeys() {
         ks.keyBindings(keyBindings);
     }
 
+
+    // --- Glyphs, Icons, and the like -----------------------------------
+
     function setUpDefs() {
-        defs = d3.select('#ov-topo svg').append('defs');
+        defs = svg.append('defs');
         gs.loadDefs(defs);
     }
 
 
+    // --- Pan and Zoom --------------------------------------------------
+
+    // zoom enabled predicate. ev is a D3 source event.
+    function zoomEnabled(ev) {
+        return (ev.metaKey || ev.altKey);
+    }
+
+    function zoomCallback() {
+        var tr = zoomer.translate(),
+            sc = zoomer.scale();
+        $log.log('ZOOM: translate = ' + tr + ', scale = ' + sc);
+
+        // TODO: keep the map lines constant width while zooming
+        //bgImg.style('stroke-width', 2.0 / scale + 'px');
+    }
+
+    function setUpZoom() {
+        var zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer');
+        zoomer = zs.createZoomer({
+            svg: svg,
+            zoomLayer: zoomLayer,
+            zoomEnabled: zoomEnabled,
+            zoomCallback: zoomCallback
+        });
+    }
+
+
+    // --- Controller Definition -----------------------------------------
+
     angular.module('ovTopo', moduleDependencies)
 
         .controller('OvTopoCtrl', [
-            '$log', 'KeyService', 'GlyphService',
+            '$log', 'KeyService', 'ZoomService', 'GlyphService',
 
-        function (_$log_, _ks_, _gs_) {
+        function (_$log_, _ks_, _zs_, _gs_) {
             var self = this;
-
             $log = _$log_;
             ks = _ks_;
+            zs = _zs_;
             gs = _gs_;
 
+            // exported state
             self.message = 'Topo View Rocks!';
 
+            // svg layer and initialization of components
+            svg = d3.select('#ov-topo svg');
             setUpKeys();
             setUpDefs();
+            setUpZoom();
 
             $log.log('OvTopoCtrl has been created');
         }]);