GUI -- Further work on MapService and GeoDataService. Still WIP.

Change-Id: I92e826cc15cc1a07238cc4b4eac20583260a3c84
diff --git a/web/gui/src/main/webapp/app/fw/svg/geodata.js b/web/gui/src/main/webapp/app/fw/svg/geodata.js
index 244507a..4d3c4a2 100644
--- a/web/gui/src/main/webapp/app/fw/svg/geodata.js
+++ b/web/gui/src/main/webapp/app/fw/svg/geodata.js
@@ -21,18 +21,36 @@
  */
 
 /*
- The GeoData Service caches GeoJSON map data, and provides supporting
- projections for mapping into SVG layers.
+ The GeoData Service facilitates the fetching and caching of TopoJSON data
+ from the server, as well as providing a way of creating a path generator
+ for that data, to be used to render the map in an SVG layer.
 
- A GeoMap object can be fetched by ID. IDs that start with an asterisk
+ A TopoData object can be fetched by ID. IDs that start with an asterisk
  identify maps bundled with the GUI. IDs that do not start with an
- asterisk are assumed to be URLs to externally provided data (exact
- format to be decided).
+ asterisk are assumed to be URLs to externally provided data.
 
- e.g.  var geomap = GeoDataService.fetchGeoMap('*continental-us');
+     var topodata = GeoDataService.fetchTopoData('*continental-us');
 
- Note that, since the GeoMap instance is cached / shared, it should
- contain no state.
+ The path generator can then be created for that data-set:
+
+     var gen = GeoDataService.createPathGenerator(topodata, opts);
+
+ opts is an optional argument that allows the override of default settings:
+     {
+         objectTag: 'states',
+         projection: d3.geo.mercator(),
+         logicalSize: 1000,
+         mapFillScale: .95
+     };
+
+ The returned object (gen) comprises transformed data (TopoJSON -> GeoJSON),
+ the D3 path generator function, and the settings used ...
+
+    {
+        geodata:  { ... },
+        pathgen:  function (...) { ... },
+        settings: { ... }
+    }
  */
 
 (function () {
@@ -66,9 +84,9 @@
 
             // returns a promise decorated with:
             //   .meta -- id, url, and whether the data was cached
-            //   .mapdata -- geojson data (on response from server)
+            //   .topodata -- TopoJSON data (on response from server)
 
-            function fetchGeoMap(id) {
+            function fetchTopoData(id) {
                 if (!fs.isS(id)) {
                     return null;
                 }
@@ -88,10 +106,10 @@
 
                     promise.then(function (response) {
                         // success
-                        promise.mapdata = response.data;
+                        promise.topodata = response.data;
                     }, function (response) {
                         // error
-                        $log.warn('Failed to retrieve map data: ' + url,
+                        $log.warn('Failed to retrieve map TopoJSON data: ' + url,
                             response.status, response.data);
                     });
 
@@ -104,15 +122,32 @@
                 return promise;
             }
 
-            // TODO: clean up implementation of projection...
-            function setProjForView(path, topoData) {
-                var dim = 1000;
+            var defaultGenSettings = {
+                objectTag: 'states',
+                projection: d3.geo.mercator(),
+                logicalSize: 1000,
+                mapFillScale: .95
+            };
+
+            // converts given TopoJSON-format data into corresponding GeoJSON
+            //  data, and creates a path generator for that data.
+            function createPathGenerator(topoData, opts) {
+                var settings = $.extend({}, defaultGenSettings, opts),
+                    topoObject = topoData.objects[settings.objectTag],
+                    geoData = topojson.feature(topoData, topoObject),
+                    proj = settings.projection,
+                    dim = settings.logicalSize,
+                    mfs = settings.mapFillScale,
+                    path = d3.geo.path().projection(proj);
+
+                // adjust projection scale and translation to fill the view
+                // with the map
 
                 // start with unit scale, no translation..
-                geoMapProj.scale(1).translate([0, 0]);
+                proj.scale(1).translate([0, 0]);
 
                 // figure out dimensions of map data..
-                var b = path.bounds(topoData),
+                var b = path.bounds(geoData),
                     x1 = b[0][0],
                     y1 = b[0][1],
                     x2 = b[1][0],
@@ -123,17 +158,24 @@
                     y = (y1 + y2) / 2;
 
                 // size map to 95% of minimum dimension to fill space..
-                var s = .95 / Math.min(dx / dim, dy / dim);
-                var t = [dim / 2 - s * x, dim / 2 - s * y];
+                var s = mfs / Math.min(dx / dim, dy / dim),
+                    t = [dim / 2 - s * x, dim / 2 - s * y];
 
                 // set new scale, translation on the projection..
-                geoMapProj.scale(s).translate(t);
-            }
+                proj.scale(s).translate(t);
 
+                // return the results
+                return {
+                    geodata: geoData,
+                    pathgen: path,
+                    settings: settings
+                };
+            }
 
             return {
                 clearCache: clearCache,
-                fetchGeoMap: fetchGeoMap
+                fetchTopoData: fetchTopoData,
+                createPathGenerator: createPathGenerator
             };
         }]);
 }());
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/app/fw/svg/map.js b/web/gui/src/main/webapp/app/fw/svg/map.js
index d57e65d..366204c 100644
--- a/web/gui/src/main/webapp/app/fw/svg/map.js
+++ b/web/gui/src/main/webapp/app/fw/svg/map.js
@@ -27,126 +27,40 @@
     e.g.  var ok = MapService.loadMapInto(svgLayer, '*continental-us');
 
     The Map Service makes use of the GeoDataService to load the required data
-    from the server.
+    from the server and to create the appropriate geographical projection.
+
 */
 
 (function () {
     'use strict';
 
     // injected references
-    var $log, $http, fs;
-
-    // internal state
-    var mapCache = d3.map(),
-        bundledUrlPrefix = '../data/map/';
-
-    function getUrl(id) {
-        if (id[0] === '*') {
-            return bundledUrlPrefix + id.slice(1) + '.json';
-        }
-        return id + '.json';
-    }
+    var $log, fs, gds;
 
     angular.module('onosSvg')
-        .factory('MapService', ['$log', '$http', 'FnService',
-        function (_$log_, _$http_, _fs_) {
+        .factory('MapService', ['$log', 'FnService', 'GeoDataService',
+        function (_$log_, _fs_, _gds_) {
             $log = _$log_;
-            $http = _$http_;
             fs = _fs_;
-
-
-            function fetchGeoMap(id) {
-                if (!fs.isS(id)) {
-                    return null;
-                }
-                var url = getUrl(id),
-                    promise = mapCache.get(id);
-
-                if (!promise) {
-                    // need to fetch the data, build the object,
-                    // cache it, and return it.
-                    promise = $http.get(url);
-
-                    promise.meta = {
-                        id: id,
-                        url: url,
-                        wasCached: false
-                    };
-
-                    promise.then(function (response) {
-                            // success
-                            promise.mapdata = response.data;
-                        }, function (response) {
-                            // error
-                            $log.warn('Failed to retrieve map data: ' + url,
-                                response.status, response.data);
-                        });
-
-                    mapCache.set(id, promise);
-
-                } else {
-                    promise.meta.wasCached = true;
-                }
-
-                return promise;
-            }
-
-            var geoMapProj;
-
-            function setProjForView(path, topoData) {
-                var dim = 1000;
-
-                // start with unit scale, no translation..
-                geoMapProj.scale(1).translate([0, 0]);
-
-                // figure out dimensions of map data..
-                var b = path.bounds(topoData),
-                    x1 = b[0][0],
-                    y1 = b[0][1],
-                    x2 = b[1][0],
-                    y2 = b[1][1],
-                    dx = x2 - x1,
-                    dy = y2 - y1,
-                    x = (x1 + x2) / 2,
-                    y = (y1 + y2) / 2;
-
-                // size map to 95% of minimum dimension to fill space..
-                var s = .95 / Math.min(dx / dim, dy / dim);
-                var t = [dim / 2 - s * x, dim / 2 - s * y];
-
-                // set new scale, translation on the projection..
-                geoMapProj.scale(s).translate(t);
-            }
-
+            gds = _gds_;
 
             function loadMapInto(mapLayer, id) {
-                var mapObject = fetchGeoMap(id);
-                if (!mapObject) {
+                var promise = gds.fetchTopoData(id);
+                if (!promise) {
                     $log.warn('Failed to load map: ' + id);
-                    return null;
+                    return false;
                 }
 
-                var mapdata = mapObject.mapdata,
-                    topoData, path;
-
-                mapObject.then(function () {
-                    // extracts the topojson data into geocoordinate-based geometry
-                    topoData = topojson.feature(mapdata, mapdata.objects.states);
-
-                    // see: http://bl.ocks.org/mbostock/4707858
-                    geoMapProj = d3.geo.mercator();
-                    path = d3.geo.path().projection(geoMapProj);
-
-                    setProjForView(path, topoData);
+                promise.then(function () {
+                    var gen = gds.createPathGenerator(promise.topodata);
 
                     mapLayer.selectAll('path')
-                        .data(topoData.features)
+                        .data(gen.geodata.features)
                         .enter()
                         .append('path')
-                        .attr('d', path);
+                        .attr('d', gen.pathgen);
                 });
-                // TODO: review whether we should just return true (not the map object)
-                return mapObject;
+                return true;
             }
 
             return {
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 cca389c..a5bec59 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -32,7 +32,7 @@
     var $log, ks, zs, gs, ms;
 
     // DOM elements
-    var svg, defs;
+    var svg, defs, zoomLayer, map;
 
     // Internal state
     var zoomer;
@@ -91,7 +91,7 @@
     }
 
     function setUpZoom() {
-        var zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer');
+        zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer');
         zoomer = zs.createZoomer({
             svg: svg,
             zoomLayer: zoomLayer,
@@ -101,6 +101,13 @@
     }
 
 
+    // --- Background Map ------------------------------------------------
+
+    function setUpMap() {
+        map = zoomLayer.append('g').attr('id', '#topo-map');
+        ms.loadMapInto(map, '*continental_us');
+    }
+
     // --- Controller Definition -----------------------------------------
 
     angular.module('ovTopo', moduleDependencies)
@@ -124,6 +131,7 @@
             setUpKeys();
             setUpDefs();
             setUpZoom();
+            setUpMap();
 
             $log.log('OvTopoCtrl has been created');
         }]);
diff --git a/web/gui/src/main/webapp/tests/app/fw/svg/geodata-spec.js b/web/gui/src/main/webapp/tests/app/fw/svg/geodata-spec.js
index c88a1b0..4333624 100644
--- a/web/gui/src/main/webapp/tests/app/fw/svg/geodata-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/svg/geodata-spec.js
@@ -39,18 +39,18 @@
 
     it('should define api functions', function () {
         expect(fs.areFunctions(gds, [
-            'clearCache', 'fetchGeoMap'
+            'clearCache', 'fetchTopoData', 'createPathGenerator'
         ])).toBeTruthy();
     });
 
     it('should return null when no parameters given', function () {
-        promise = gds.fetchGeoMap();
+        promise = gds.fetchTopoData();
         expect(promise).toBeNull();
     });
 
     it('should augment the id of a bundled map', function () {
         var id = '*foo';
-        promise = gds.fetchGeoMap(id);
+        promise = gds.fetchTopoData(id);
         expect(promise.meta).toBeDefined();
         expect(promise.meta.id).toBe(id);
         expect(promise.meta.url).toBe('../data/map/foo.json');
@@ -58,7 +58,7 @@
 
     it('should treat an external id as the url itself', function () {
         var id = 'some/path/to/foo';
-        promise = gds.fetchGeoMap(id);
+        promise = gds.fetchTopoData(id);
         expect(promise.meta).toBeDefined();
         expect(promise.meta.id).toBe(id);
         expect(promise.meta.url).toBe(id + '.json');
@@ -66,14 +66,14 @@
 
     it('should cache the returned objects', function () {
         var id = 'foo';
-        promise = gds.fetchGeoMap(id);
+        promise = gds.fetchTopoData(id);
         expect(promise).toBeDefined();
         expect(promise.meta.wasCached).toBeFalsy();
         expect(promise.tagged).toBeUndefined();
 
         promise.tagged = 'I woz here';
 
-        promise = gds.fetchGeoMap(id);
+        promise = gds.fetchTopoData(id);
         expect(promise).toBeDefined();
         expect(promise.meta.wasCached).toBeTruthy();
         expect(promise.tagged).toEqual('I woz here');
@@ -81,14 +81,14 @@
 
     it('should clear the cache when asked', function () {
         var id = 'foo';
-        promise = gds.fetchGeoMap(id);
+        promise = gds.fetchTopoData(id);
         expect(promise.meta.wasCached).toBeFalsy();
 
-        promise = gds.fetchGeoMap(id);
+        promise = gds.fetchTopoData(id);
         expect(promise.meta.wasCached).toBeTruthy();
 
         gds.clearCache();
-        promise = gds.fetchGeoMap(id);
+        promise = gds.fetchTopoData(id);
         expect(promise.meta.wasCached).toBeFalsy();
     });
 
@@ -98,12 +98,64 @@
         $httpBackend.expectGET('foo.json').respond(404, 'Not found');
         spyOn($log, 'warn');
 
-        promise = gds.fetchGeoMap(id);
+        promise = gds.fetchTopoData(id);
         $httpBackend.flush();
-        expect(promise.mapdata).toBeUndefined();
+        expect(promise.topodata).toBeUndefined();
         expect($log.warn)
-            .toHaveBeenCalledWith('Failed to retrieve map data: foo.json',
+            .toHaveBeenCalledWith('Failed to retrieve map TopoJSON data: foo.json',
             404, 'Not found');
     });
 
+    // --- path generator tests
+
+    function simpleTopology(object) {
+        return {
+            type: "Topology",
+            transform: {scale: [1, 1], translate: [0, 0]},
+            objects: {states: object},
+            arcs: [
+                [[0, 0], [1, 0], [0, 1], [-1, 0], [0, -1]],
+                [[0, 0], [1, 0], [0, 1]],
+                [[1, 1], [-1, 0], [0, -1]],
+                [[1, 1]],
+                [[0, 0]]
+            ]
+        };
+    }
+
+    function simpleLineStringTopo() {
+        return simpleTopology({type: "LineString", arcs: [1, 2]});
+    }
+
+    it('should use default settings if none are supplied', function () {
+        var gen = gds.createPathGenerator(simpleLineStringTopo());
+        expect(gen.settings.objectTag).toBe('states');
+        expect(gen.settings.logicalSize).toBe(1000);
+        expect(gen.settings.mapFillScale).toBe(.95);
+        // best we can do for now is test that projection is a function ...
+        expect(fs.isF(gen.settings.projection)).toBeTruthy();
+    });
+
+    it('should allow us to override default settings', function () {
+        var gen = gds.createPathGenerator(simpleLineStringTopo(), {
+            mapFillScale: .80
+        });
+        expect(gen.settings.objectTag).toBe('states');
+        expect(gen.settings.logicalSize).toBe(1000);
+        expect(gen.settings.mapFillScale).toBe(.80);
+    });
+
+    it('should create transformed geodata, and a path generator', function () {
+        var gen = gds.createPathGenerator(simpleLineStringTopo());
+        expect(fs.isO(gen.settings)).toBeTruthy();
+        expect(fs.isO(gen.geodata)).toBeTruthy();
+        expect(fs.isF(gen.pathgen)).toBeTruthy();
+    });
+    // NOTE: we probably should have more unit tests that assert stuff about
+    //       the transformed data (geo data) -- though perhaps we can rely on
+    //       the unit testing of TopoJSON? See...
+    //  https://github.com/mbostock/topojson/blob/master/test/feature-test.js
+    //       and, what about the path generator?, and the computed bounds?
+    //  In summary, more work should be done here..
+
 });