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/d3Utils.js b/web/gui/src/main/webapp/d3Utils.js
index 51651fa..e647a37 100644
--- a/web/gui/src/main/webapp/d3Utils.js
+++ b/web/gui/src/main/webapp/d3Utils.js
@@ -23,7 +23,7 @@
 (function (onos) {
     'use strict';
 
-    function createDragBehavior(force, selectCb, atDragEnd) {
+    function createDragBehavior(force, selectCb, atDragEnd, requireMeta) {
         var draggedThreshold = d3.scale.linear()
                 .domain([0, 0.1])
                 .range([5, 20])
@@ -51,29 +51,39 @@
         drag = d3.behavior.drag()
             .origin(function(d) { return d; })
             .on('dragstart', function(d) {
-                d.oldX = d.x;
-                d.oldY = d.y;
-                d.dragged = false;
-                d.fixed |= 2;
+                if (requireMeta ^ !d3.event.sourceEvent.metaKey) {
+                    d3.event.sourceEvent.stopPropagation();
+
+                    d.oldX = d.x;
+                    d.oldY = d.y;
+                    d.dragged = false;
+                    d.fixed |= 2;
+                    d.dragStarted = true;
+                }
             })
             .on('drag', function(d) {
-                d.px = d3.event.x;
-                d.py = d3.event.y;
-                if (dragged(d)) {
-                    if (!force.alpha()) {
-                        force.alpha(.025);
+                if (requireMeta ^ !d3.event.sourceEvent.metaKey) {
+                    d.px = d3.event.x;
+                    d.py = d3.event.y;
+                    if (dragged(d)) {
+                        if (!force.alpha()) {
+                            force.alpha(.025);
+                        }
                     }
                 }
             })
             .on('dragend', function(d) {
-                if (!dragged(d)) {
-                    // consider this the same as a 'click' (selection of node)
-                    selectCb(d, this); // TODO: set 'this' context instead of param
-                }
-                d.fixed &= ~6;
+                if (d.dragStarted) {
+                    d.dragStarted = false;
+                    if (!dragged(d)) {
+                        // consider this the same as a 'click' (selection of node)
+                        selectCb(d, this); // TODO: set 'this' context instead of param
+                    }
+                    d.fixed &= ~6;
 
-                // hook at the end of a drag gesture
-                atDragEnd(d, this); // TODO: set 'this' context instead of param
+                    // hook at the end of a drag gesture
+                    atDragEnd(d, this); // TODO: set 'this' context instead of param
+                }
             });
 
         return drag;
diff --git a/web/gui/src/main/webapp/json/map/README.txt b/web/gui/src/main/webapp/json/map/README.txt
index 1f72bae..5cd2634 100644
--- a/web/gui/src/main/webapp/json/map/README.txt
+++ b/web/gui/src/main/webapp/json/map/README.txt
@@ -5,11 +5,57 @@
 
 To generate continental US map:
 
-$ wget 'http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/cultural/ne_50m_admin_1_states_provinces_lakes.zip'
-$ unzip ne_50m_admin_1_states_provinces_lakes.zip
-$ ogr2ogr -f GeoJSON -where "sr_adm0_a3 IN ('USA')" states.json ne_50m_admin_1_states_provinces_lakes.shp
+    $ wget 'http://www.naturalearthdata.com/download/50m/cultural/ne_50m_admin_1_states_provinces_lakes.zip'
+    $ unzip ne_50m_admin_1_states_provinces_lakes.zip
+    $ ogr2ogr -f GeoJSON -where "sr_adm0_a3 IN ('USA')" states.json ne_50m_admin_1_states_provinces_lakes.shp
 
 edit states.json to remove data for Hawaii and Alaska
 
-$ topojson states.json > topology.json
+    $ topojson states.json > topology.json
+
+
+The .shp file above is incomplete (USA and part of Candada.)
+So it may be that each region requires a bit of research to generate.
+Ideally a source for public domain shp files can be found that covers all geographic regions.
+
+
+For Canada:
+
+    # wget 'http://www12.statcan.gc.ca/census-recensement/2011/geo/bound-limit/files-fichiers/gpr_000b11a_e.zip'
+    # unzip gpr_000b11a_e.zip
+    # ogr2ogr -f "GeoJSON" -s_srs EPSG:21781 -t_srs EPSG:4326 canada.json gpr_000b11a_e.shp
+    # topojson --id-property CFSAUID -p name=PRNAME -p name canada.json > topology.json
+
+
+This produces a very large (5MB) file and draws very slowly in Chrome.
+So some additional processing is required to simplify the geometry. (It is not checked in.)
+
+Also, the specification of object structure within the geojson is unclear.
+In the US map the geojson structure is
+
+    json.objects.states
+
+but in the Canadian data it's
+
+    json.objects.canada
+
+
+Lastly, the projection that is used may be tailored to the region.
+The preferred projection for the US is "albers" and d3 provides a "albersUSA" which can be used to
+    project hawaii and alaska as well
+
+For Canada, apparantly a "Lambert" projection (called conicConformal in d3) is preferred
+
+see:
+    https://github.com/mbostock/d3/wiki/Geo-Projections
+    http://www.statcan.gc.ca/pub/92-195-x/2011001/other-autre/mapproj-projcarte/m-c-eng.htm
+
+
+Summary:
+- some additional work is required to fully generalize maps functionality.
+- it may be worthwhile for ON.LAB to provide the topo files for key regions since producing these
+    files is non-trivial
+
+
+
 
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()