better readme, do scaling properly
implement pan/zoom on topo
R to reset pan/zoom
require meta to drag or select nodes

Change-Id: I15e20296e76d5cd8656b144b2d61a6923a5509ad
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index d87cd27..41fcd6b 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -128,6 +128,7 @@
         L: cycleLabels,
         P: togglePorts,
         U: unpin,
+        R: resetZoomPan,
 
         W: requestTraffic,  // bag of selections
         X: cancelTraffic,
@@ -168,6 +169,7 @@
 
     // D3 selections
     var svg,
+        zoomPanContainer,
         bgImg,
         topoG,
         nodeG,
@@ -179,6 +181,9 @@
     // the projection for the map background
     var geoMapProjection;
 
+    // the zoom function
+    var zoom;
+
     // ==============================
     // For Debugging / Development
 
@@ -1358,6 +1363,33 @@
         });
     }
 
+    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");
+    }
+
+    function resetZoomPan() {
+        zoomPan(1, [0,0]);
+        zoom.scale(1).translate([0,0]);
+    }
+
+    function setupZoomPan() {
+        function zoomed() {
+            if (!d3.event.sourceEvent.metaKey) {
+                zoomPan(d3.event.scale, d3.event.translate);
+            }
+        }
+
+        zoom = d3.behavior.zoom()
+            .translate([0, 0])
+            .scale(1)
+            .scaleExtent([1, 8])
+            .on("zoom", zoomed);
+
+        svg.call(zoom);
+    }
+
     // ==============================
     // Test harness code
 
@@ -1438,11 +1470,15 @@
         svg = view.$div.append('svg').attr('viewBox', viewBox);
         setSize(svg, view);
 
+        zoomPanContainer = svg.append('g').attr('id', 'zoomPanContainer');
+
+        setupZoomPan();
+
         // add blue glow filter to svg layer
-        d3u.appendGlow(svg);
+        d3u.appendGlow(zoomPanContainer);
 
         // group for the topology
-        topoG = svg.append('g')
+        topoG = zoomPanContainer.append('g')
             .attr('id', 'topo-G')
             .attr('transform', fcfg.translate());
 
@@ -1501,7 +1537,7 @@
             .linkStrength(lstrg)
             .on('tick', tick);
 
-        network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
+        network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd, true); // true=require meta
 
         // create mask layer for when we lose connection to server.
         mask = view.$div.append('div').attr('id','topo-mask');
@@ -1593,15 +1629,15 @@
 
         // [[x1,y1],[x2,y2]]
         var b = path.bounds(topoData);
-        // TODO: why 1.75?
-        var s = 1.75 / Math.max((b[1][0] - b[0][0]) / config.logicalSize, (b[1][1] - b[0][1]) / config.logicalSize);
+        // size map to 95% of minimum dimension to fill space
+        var s = .95 / Math.min((b[1][0] - b[0][0]) / config.logicalSize, (b[1][1] - b[0][1]) / config.logicalSize);
         var t = [(config.logicalSize - s * (b[1][0] + b[0][0])) / 2, (config.logicalSize - s * (b[1][1] + b[0][1])) / 2];
 
         geoMapProjection
             .scale(s)
             .translate(t);
 
-        bgImg = svg.insert("g", '#topo-G');
+        bgImg = zoomPanContainer.insert("g", '#topo-G');
         bgImg.attr('id', 'map').selectAll('path')
             .data(topoData.features)
             .enter()