GUI -- Split MapService into GeoDataService and MapService. WIP.

Change-Id: Ibfe5b35ecdfaaf39b9d48abd29d0a44327dec130
diff --git a/web/gui/src/main/webapp/app/fw/svg/geodata.js b/web/gui/src/main/webapp/app/fw/svg/geodata.js
new file mode 100644
index 0000000..244507a
--- /dev/null
+++ b/web/gui/src/main/webapp/app/fw/svg/geodata.js
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- SVG -- GeoData Service
+
+ @author Simon Hunt
+ */
+
+/*
+ The GeoData Service caches GeoJSON map data, and provides supporting
+ projections for mapping into SVG layers.
+
+ A GeoMap 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).
+
+ e.g.  var geomap = GeoDataService.fetchGeoMap('*continental-us');
+
+ Note that, since the GeoMap instance is cached / shared, it should
+ contain no state.
+ */
+
+(function () {
+    'use strict';
+
+    // injected references
+    var $log, $http, fs;
+
+    // internal state
+    var cache = d3.map(),
+        bundledUrlPrefix = '../data/map/';
+
+    function getUrl(id) {
+        if (id[0] === '*') {
+            return bundledUrlPrefix + id.slice(1) + '.json';
+        }
+        return id + '.json';
+    }
+
+    angular.module('onosSvg')
+        .factory('GeoDataService', ['$log', '$http', 'FnService',
+        function (_$log_, _$http_, _fs_) {
+            $log = _$log_;
+            $http = _$http_;
+            fs = _fs_;
+
+            // start afresh...
+            function clearCache() {
+                cache = d3.map();
+            }
+
+            // returns a promise decorated with:
+            //   .meta -- id, url, and whether the data was cached
+            //   .mapdata -- geojson data (on response from server)
+
+            function fetchGeoMap(id) {
+                if (!fs.isS(id)) {
+                    return null;
+                }
+                var url = getUrl(id),
+                    promise = cache.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);
+                    });
+
+                    cache.set(id, promise);
+
+                } else {
+                    promise.meta.wasCached = true;
+                }
+
+                return promise;
+            }
+
+            // TODO: clean up implementation of projection...
+            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);
+            }
+
+
+            return {
+                clearCache: clearCache,
+                fetchGeoMap: fetchGeoMap
+            };
+        }]);
+}());
\ 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 7a6b981..d57e65d 100644
--- a/web/gui/src/main/webapp/app/fw/svg/map.js
+++ b/web/gui/src/main/webapp/app/fw/svg/map.js
@@ -21,22 +21,14 @@
  */
 
 /*
-    The Map Service caches GeoJSON maps, which can be loaded into the map
-    layer of the Topology View.
+    The Map Service provides a simple API for loading geographical maps into
+    an SVG layer. For example, as a background to the Topology View.
 
-    A GeoMap 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).
+    e.g.  var ok = MapService.loadMapInto(svgLayer, '*continental-us');
 
-    e.g.  var geomap = MapService.fetchGeoMap('*continental-us');
-
-    The GeoMap object encapsulates topology data (features), and the
-    D3 projection object.
-
-    Note that, since the GeoMap instance is cached / shared, it should
-    contain no state.
- */
+    The Map Service makes use of the GeoDataService to load the required data
+    from the server.
+*/
 
 (function () {
     'use strict';
diff --git a/web/gui/src/main/webapp/app/index.html b/web/gui/src/main/webapp/app/index.html
index d261b3f..5fa0364 100644
--- a/web/gui/src/main/webapp/app/index.html
+++ b/web/gui/src/main/webapp/app/index.html
@@ -44,6 +44,7 @@
     <script src="fw/svg/svg.js"></script>
     <script src="fw/svg/glyph.js"></script>
     <script src="fw/svg/icon.js"></script>
+    <script src="fw/svg/geodata.js"></script>
     <script src="fw/svg/map.js"></script>
     <script src="fw/svg/zoom.js"></script>
 
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
new file mode 100644
index 0000000..c88a1b0
--- /dev/null
+++ b/web/gui/src/main/webapp/tests/app/fw/svg/geodata-spec.js
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- SVG -- GeoData Service - Unit Tests
+
+ @author Simon Hunt
+ */
+describe('factory: fw/svg/geodata.js', function() {
+    var $log, $httpBackend, fs, gds, promise;
+
+    beforeEach(module('onosUtil', 'onosSvg'));
+
+    beforeEach(inject(function (_$log_, _$httpBackend_, FnService, GeoDataService) {
+        $log = _$log_;
+        $httpBackend = _$httpBackend_;
+        fs = FnService;
+        gds = GeoDataService;
+        gds.clearCache();
+    }));
+
+
+    it('should define GeoDataService', function () {
+        expect(gds).toBeDefined();
+    });
+
+    it('should define api functions', function () {
+        expect(fs.areFunctions(gds, [
+            'clearCache', 'fetchGeoMap'
+        ])).toBeTruthy();
+    });
+
+    it('should return null when no parameters given', function () {
+        promise = gds.fetchGeoMap();
+        expect(promise).toBeNull();
+    });
+
+    it('should augment the id of a bundled map', function () {
+        var id = '*foo';
+        promise = gds.fetchGeoMap(id);
+        expect(promise.meta).toBeDefined();
+        expect(promise.meta.id).toBe(id);
+        expect(promise.meta.url).toBe('../data/map/foo.json');
+    });
+
+    it('should treat an external id as the url itself', function () {
+        var id = 'some/path/to/foo';
+        promise = gds.fetchGeoMap(id);
+        expect(promise.meta).toBeDefined();
+        expect(promise.meta.id).toBe(id);
+        expect(promise.meta.url).toBe(id + '.json');
+    });
+
+    it('should cache the returned objects', function () {
+        var id = 'foo';
+        promise = gds.fetchGeoMap(id);
+        expect(promise).toBeDefined();
+        expect(promise.meta.wasCached).toBeFalsy();
+        expect(promise.tagged).toBeUndefined();
+
+        promise.tagged = 'I woz here';
+
+        promise = gds.fetchGeoMap(id);
+        expect(promise).toBeDefined();
+        expect(promise.meta.wasCached).toBeTruthy();
+        expect(promise.tagged).toEqual('I woz here');
+    });
+
+    it('should clear the cache when asked', function () {
+        var id = 'foo';
+        promise = gds.fetchGeoMap(id);
+        expect(promise.meta.wasCached).toBeFalsy();
+
+        promise = gds.fetchGeoMap(id);
+        expect(promise.meta.wasCached).toBeTruthy();
+
+        gds.clearCache();
+        promise = gds.fetchGeoMap(id);
+        expect(promise.meta.wasCached).toBeFalsy();
+    });
+
+
+    it('should log a warning if data fails to load', function () {
+        var id = 'foo';
+        $httpBackend.expectGET('foo.json').respond(404, 'Not found');
+        spyOn($log, 'warn');
+
+        promise = gds.fetchGeoMap(id);
+        $httpBackend.flush();
+        expect(promise.mapdata).toBeUndefined();
+        expect($log.warn)
+            .toHaveBeenCalledWith('Failed to retrieve map data: foo.json',
+            404, 'Not found');
+    });
+
+});
diff --git a/web/gui/src/main/webapp/tests/app/fw/svg/map-spec.js b/web/gui/src/main/webapp/tests/app/fw/svg/map-spec.js
index 23746f4..7ac7857 100644
--- a/web/gui/src/main/webapp/tests/app/fw/svg/map-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/svg/map-spec.js
@@ -82,76 +82,8 @@
         // TODO: figure out how to test this function as a black box test.
 
         expect(obj).toBeTruthy();
-        debugger;
 
         // todo: assert that paths are added to map layer element
     });
 
-/*
-
-
-
-    it('should return null when no parameters given', function () {
-        promise = ms.fetchGeoMap();
-        expect(promise).toBeNull();
-    });
-
-    it('should augment the id of a bundled map', function () {
-        var id = '*foo';
-        promise = ms.fetchGeoMap(id);
-        expect(promise.meta).toBeDefined();
-        expect(promise.meta.id).toBe(id);
-        expect(promise.meta.url).toBe('../data/map/foo.json');
-    });
-
-    it('should treat an external id as the url itself', function () {
-        var id = 'some/path/to/foo';
-        promise = ms.fetchGeoMap(id);
-        expect(promise.meta).toBeDefined();
-        expect(promise.meta.id).toBe(id);
-        expect(promise.meta.url).toBe(id + '.json');
-    });
-
-    it('should cache the returned objects', function () {
-        var id = 'foo';
-        promise = ms.fetchGeoMap(id);
-        expect(promise).toBeDefined();
-        expect(promise.meta.wasCached).toBeFalsy();
-        expect(promise.tagged).toBeUndefined();
-
-        promise.tagged = 'I woz here';
-
-        promise = ms.fetchGeoMap(id);
-        expect(promise).toBeDefined();
-        expect(promise.meta.wasCached).toBeTruthy();
-        expect(promise.tagged).toEqual('I woz here');
-    });
-
-    it('should clear the cache when asked', function () {
-        var id = 'foo';
-        promise = ms.fetchGeoMap(id);
-        expect(promise.meta.wasCached).toBeFalsy();
-
-        promise = ms.fetchGeoMap(id);
-        expect(promise.meta.wasCached).toBeTruthy();
-
-        ms.clearCache();
-        promise = ms.fetchGeoMap(id);
-        expect(promise.meta.wasCached).toBeFalsy();
-    });
-
-
-    it('should log a warning if data fails to load', function () {
-        $httpBackend.expectGET(mapurl).respond(404, 'Not found');
-        spyOn($log, 'warn');
-
-        promise = ms.fetchGeoMap(mapid);
-        $httpBackend.flush();
-        expect(promise.mapdata).toBeUndefined();
-        expect($log.warn)
-            .toHaveBeenCalledWith('Failed to retrieve map data: ' + mapurl,
-                                    404, 'Not found');
-
-    });
-*/
 });