Simon Hunt

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

Change-Id: I92e826cc15cc1a07238cc4b4eac20583260a3c84
...@@ -21,18 +21,36 @@ ...@@ -21,18 +21,36 @@
21 */ 21 */
22 22
23 /* 23 /*
24 - The GeoData Service caches GeoJSON map data, and provides supporting 24 + The GeoData Service facilitates the fetching and caching of TopoJSON data
25 - projections for mapping into SVG layers. 25 + from the server, as well as providing a way of creating a path generator
26 + for that data, to be used to render the map in an SVG layer.
26 27
27 - A GeoMap object can be fetched by ID. IDs that start with an asterisk 28 + A TopoData object can be fetched by ID. IDs that start with an asterisk
28 identify maps bundled with the GUI. IDs that do not start with an 29 identify maps bundled with the GUI. IDs that do not start with an
29 - asterisk are assumed to be URLs to externally provided data (exact 30 + asterisk are assumed to be URLs to externally provided data.
30 - format to be decided).
31 31
32 - e.g. var geomap = GeoDataService.fetchGeoMap('*continental-us'); 32 + var topodata = GeoDataService.fetchTopoData('*continental-us');
33 33
34 - Note that, since the GeoMap instance is cached / shared, it should 34 + The path generator can then be created for that data-set:
35 - contain no state. 35 +
36 + var gen = GeoDataService.createPathGenerator(topodata, opts);
37 +
38 + opts is an optional argument that allows the override of default settings:
39 + {
40 + objectTag: 'states',
41 + projection: d3.geo.mercator(),
42 + logicalSize: 1000,
43 + mapFillScale: .95
44 + };
45 +
46 + The returned object (gen) comprises transformed data (TopoJSON -> GeoJSON),
47 + the D3 path generator function, and the settings used ...
48 +
49 + {
50 + geodata: { ... },
51 + pathgen: function (...) { ... },
52 + settings: { ... }
53 + }
36 */ 54 */
37 55
38 (function () { 56 (function () {
...@@ -66,9 +84,9 @@ ...@@ -66,9 +84,9 @@
66 84
67 // returns a promise decorated with: 85 // returns a promise decorated with:
68 // .meta -- id, url, and whether the data was cached 86 // .meta -- id, url, and whether the data was cached
69 - // .mapdata -- geojson data (on response from server) 87 + // .topodata -- TopoJSON data (on response from server)
70 88
71 - function fetchGeoMap(id) { 89 + function fetchTopoData(id) {
72 if (!fs.isS(id)) { 90 if (!fs.isS(id)) {
73 return null; 91 return null;
74 } 92 }
...@@ -88,10 +106,10 @@ ...@@ -88,10 +106,10 @@
88 106
89 promise.then(function (response) { 107 promise.then(function (response) {
90 // success 108 // success
91 - promise.mapdata = response.data; 109 + promise.topodata = response.data;
92 }, function (response) { 110 }, function (response) {
93 // error 111 // error
94 - $log.warn('Failed to retrieve map data: ' + url, 112 + $log.warn('Failed to retrieve map TopoJSON data: ' + url,
95 response.status, response.data); 113 response.status, response.data);
96 }); 114 });
97 115
...@@ -104,15 +122,32 @@ ...@@ -104,15 +122,32 @@
104 return promise; 122 return promise;
105 } 123 }
106 124
107 - // TODO: clean up implementation of projection... 125 + var defaultGenSettings = {
108 - function setProjForView(path, topoData) { 126 + objectTag: 'states',
109 - var dim = 1000; 127 + projection: d3.geo.mercator(),
128 + logicalSize: 1000,
129 + mapFillScale: .95
130 + };
131 +
132 + // converts given TopoJSON-format data into corresponding GeoJSON
133 + // data, and creates a path generator for that data.
134 + function createPathGenerator(topoData, opts) {
135 + var settings = $.extend({}, defaultGenSettings, opts),
136 + topoObject = topoData.objects[settings.objectTag],
137 + geoData = topojson.feature(topoData, topoObject),
138 + proj = settings.projection,
139 + dim = settings.logicalSize,
140 + mfs = settings.mapFillScale,
141 + path = d3.geo.path().projection(proj);
142 +
143 + // adjust projection scale and translation to fill the view
144 + // with the map
110 145
111 // start with unit scale, no translation.. 146 // start with unit scale, no translation..
112 - geoMapProj.scale(1).translate([0, 0]); 147 + proj.scale(1).translate([0, 0]);
113 148
114 // figure out dimensions of map data.. 149 // figure out dimensions of map data..
115 - var b = path.bounds(topoData), 150 + var b = path.bounds(geoData),
116 x1 = b[0][0], 151 x1 = b[0][0],
117 y1 = b[0][1], 152 y1 = b[0][1],
118 x2 = b[1][0], 153 x2 = b[1][0],
...@@ -123,17 +158,24 @@ ...@@ -123,17 +158,24 @@
123 y = (y1 + y2) / 2; 158 y = (y1 + y2) / 2;
124 159
125 // size map to 95% of minimum dimension to fill space.. 160 // size map to 95% of minimum dimension to fill space..
126 - var s = .95 / Math.min(dx / dim, dy / dim); 161 + var s = mfs / Math.min(dx / dim, dy / dim),
127 - var t = [dim / 2 - s * x, dim / 2 - s * y]; 162 + t = [dim / 2 - s * x, dim / 2 - s * y];
128 163
129 // set new scale, translation on the projection.. 164 // set new scale, translation on the projection..
130 - geoMapProj.scale(s).translate(t); 165 + proj.scale(s).translate(t);
131 - }
132 166
167 + // return the results
168 + return {
169 + geodata: geoData,
170 + pathgen: path,
171 + settings: settings
172 + };
173 + }
133 174
134 return { 175 return {
135 clearCache: clearCache, 176 clearCache: clearCache,
136 - fetchGeoMap: fetchGeoMap 177 + fetchTopoData: fetchTopoData,
178 + createPathGenerator: createPathGenerator
137 }; 179 };
138 }]); 180 }]);
139 }()); 181 }());
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -27,126 +27,40 @@ ...@@ -27,126 +27,40 @@
27 e.g. var ok = MapService.loadMapInto(svgLayer, '*continental-us'); 27 e.g. var ok = MapService.loadMapInto(svgLayer, '*continental-us');
28 28
29 The Map Service makes use of the GeoDataService to load the required data 29 The Map Service makes use of the GeoDataService to load the required data
30 - from the server. 30 + from the server and to create the appropriate geographical projection.
31 +
31 */ 32 */
32 33
33 (function () { 34 (function () {
34 'use strict'; 35 'use strict';
35 36
36 // injected references 37 // injected references
37 - var $log, $http, fs; 38 + var $log, fs, gds;
38 -
39 - // internal state
40 - var mapCache = d3.map(),
41 - bundledUrlPrefix = '../data/map/';
42 -
43 - function getUrl(id) {
44 - if (id[0] === '*') {
45 - return bundledUrlPrefix + id.slice(1) + '.json';
46 - }
47 - return id + '.json';
48 - }
49 39
50 angular.module('onosSvg') 40 angular.module('onosSvg')
51 - .factory('MapService', ['$log', '$http', 'FnService', 41 + .factory('MapService', ['$log', 'FnService', 'GeoDataService',
52 - function (_$log_, _$http_, _fs_) { 42 + function (_$log_, _fs_, _gds_) {
53 $log = _$log_; 43 $log = _$log_;
54 - $http = _$http_;
55 fs = _fs_; 44 fs = _fs_;
56 - 45 + gds = _gds_;
57 -
58 - function fetchGeoMap(id) {
59 - if (!fs.isS(id)) {
60 - return null;
61 - }
62 - var url = getUrl(id),
63 - promise = mapCache.get(id);
64 -
65 - if (!promise) {
66 - // need to fetch the data, build the object,
67 - // cache it, and return it.
68 - promise = $http.get(url);
69 -
70 - promise.meta = {
71 - id: id,
72 - url: url,
73 - wasCached: false
74 - };
75 -
76 - promise.then(function (response) {
77 - // success
78 - promise.mapdata = response.data;
79 - }, function (response) {
80 - // error
81 - $log.warn('Failed to retrieve map data: ' + url,
82 - response.status, response.data);
83 - });
84 -
85 - mapCache.set(id, promise);
86 -
87 - } else {
88 - promise.meta.wasCached = true;
89 - }
90 -
91 - return promise;
92 - }
93 -
94 - var geoMapProj;
95 -
96 - function setProjForView(path, topoData) {
97 - var dim = 1000;
98 -
99 - // start with unit scale, no translation..
100 - geoMapProj.scale(1).translate([0, 0]);
101 -
102 - // figure out dimensions of map data..
103 - var b = path.bounds(topoData),
104 - x1 = b[0][0],
105 - y1 = b[0][1],
106 - x2 = b[1][0],
107 - y2 = b[1][1],
108 - dx = x2 - x1,
109 - dy = y2 - y1,
110 - x = (x1 + x2) / 2,
111 - y = (y1 + y2) / 2;
112 -
113 - // size map to 95% of minimum dimension to fill space..
114 - var s = .95 / Math.min(dx / dim, dy / dim);
115 - var t = [dim / 2 - s * x, dim / 2 - s * y];
116 -
117 - // set new scale, translation on the projection..
118 - geoMapProj.scale(s).translate(t);
119 - }
120 -
121 46
122 function loadMapInto(mapLayer, id) { 47 function loadMapInto(mapLayer, id) {
123 - var mapObject = fetchGeoMap(id); 48 + var promise = gds.fetchTopoData(id);
124 - if (!mapObject) { 49 + if (!promise) {
125 $log.warn('Failed to load map: ' + id); 50 $log.warn('Failed to load map: ' + id);
126 - return null; 51 + return false;
127 } 52 }
128 53
129 - var mapdata = mapObject.mapdata, 54 + promise.then(function () {
130 - topoData, path; 55 + var gen = gds.createPathGenerator(promise.topodata);
131 -
132 - mapObject.then(function () {
133 - // extracts the topojson data into geocoordinate-based geometry
134 - topoData = topojson.feature(mapdata, mapdata.objects.states);
135 -
136 - // see: http://bl.ocks.org/mbostock/4707858
137 - geoMapProj = d3.geo.mercator();
138 - path = d3.geo.path().projection(geoMapProj);
139 -
140 - setProjForView(path, topoData);
141 56
142 mapLayer.selectAll('path') 57 mapLayer.selectAll('path')
143 - .data(topoData.features) 58 + .data(gen.geodata.features)
144 .enter() 59 .enter()
145 .append('path') 60 .append('path')
146 - .attr('d', path); 61 + .attr('d', gen.pathgen);
147 }); 62 });
148 - // TODO: review whether we should just return true (not the map object) 63 + return true;
149 - return mapObject;
150 } 64 }
151 65
152 return { 66 return {
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
32 var $log, ks, zs, gs, ms; 32 var $log, ks, zs, gs, ms;
33 33
34 // DOM elements 34 // DOM elements
35 - var svg, defs; 35 + var svg, defs, zoomLayer, map;
36 36
37 // Internal state 37 // Internal state
38 var zoomer; 38 var zoomer;
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
91 } 91 }
92 92
93 function setUpZoom() { 93 function setUpZoom() {
94 - var zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer'); 94 + zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer');
95 zoomer = zs.createZoomer({ 95 zoomer = zs.createZoomer({
96 svg: svg, 96 svg: svg,
97 zoomLayer: zoomLayer, 97 zoomLayer: zoomLayer,
...@@ -101,6 +101,13 @@ ...@@ -101,6 +101,13 @@
101 } 101 }
102 102
103 103
104 + // --- Background Map ------------------------------------------------
105 +
106 + function setUpMap() {
107 + map = zoomLayer.append('g').attr('id', '#topo-map');
108 + ms.loadMapInto(map, '*continental_us');
109 + }
110 +
104 // --- Controller Definition ----------------------------------------- 111 // --- Controller Definition -----------------------------------------
105 112
106 angular.module('ovTopo', moduleDependencies) 113 angular.module('ovTopo', moduleDependencies)
...@@ -124,6 +131,7 @@ ...@@ -124,6 +131,7 @@
124 setUpKeys(); 131 setUpKeys();
125 setUpDefs(); 132 setUpDefs();
126 setUpZoom(); 133 setUpZoom();
134 + setUpMap();
127 135
128 $log.log('OvTopoCtrl has been created'); 136 $log.log('OvTopoCtrl has been created');
129 }]); 137 }]);
......
...@@ -39,18 +39,18 @@ describe('factory: fw/svg/geodata.js', function() { ...@@ -39,18 +39,18 @@ describe('factory: fw/svg/geodata.js', function() {
39 39
40 it('should define api functions', function () { 40 it('should define api functions', function () {
41 expect(fs.areFunctions(gds, [ 41 expect(fs.areFunctions(gds, [
42 - 'clearCache', 'fetchGeoMap' 42 + 'clearCache', 'fetchTopoData', 'createPathGenerator'
43 ])).toBeTruthy(); 43 ])).toBeTruthy();
44 }); 44 });
45 45
46 it('should return null when no parameters given', function () { 46 it('should return null when no parameters given', function () {
47 - promise = gds.fetchGeoMap(); 47 + promise = gds.fetchTopoData();
48 expect(promise).toBeNull(); 48 expect(promise).toBeNull();
49 }); 49 });
50 50
51 it('should augment the id of a bundled map', function () { 51 it('should augment the id of a bundled map', function () {
52 var id = '*foo'; 52 var id = '*foo';
53 - promise = gds.fetchGeoMap(id); 53 + promise = gds.fetchTopoData(id);
54 expect(promise.meta).toBeDefined(); 54 expect(promise.meta).toBeDefined();
55 expect(promise.meta.id).toBe(id); 55 expect(promise.meta.id).toBe(id);
56 expect(promise.meta.url).toBe('../data/map/foo.json'); 56 expect(promise.meta.url).toBe('../data/map/foo.json');
...@@ -58,7 +58,7 @@ describe('factory: fw/svg/geodata.js', function() { ...@@ -58,7 +58,7 @@ describe('factory: fw/svg/geodata.js', function() {
58 58
59 it('should treat an external id as the url itself', function () { 59 it('should treat an external id as the url itself', function () {
60 var id = 'some/path/to/foo'; 60 var id = 'some/path/to/foo';
61 - promise = gds.fetchGeoMap(id); 61 + promise = gds.fetchTopoData(id);
62 expect(promise.meta).toBeDefined(); 62 expect(promise.meta).toBeDefined();
63 expect(promise.meta.id).toBe(id); 63 expect(promise.meta.id).toBe(id);
64 expect(promise.meta.url).toBe(id + '.json'); 64 expect(promise.meta.url).toBe(id + '.json');
...@@ -66,14 +66,14 @@ describe('factory: fw/svg/geodata.js', function() { ...@@ -66,14 +66,14 @@ describe('factory: fw/svg/geodata.js', function() {
66 66
67 it('should cache the returned objects', function () { 67 it('should cache the returned objects', function () {
68 var id = 'foo'; 68 var id = 'foo';
69 - promise = gds.fetchGeoMap(id); 69 + promise = gds.fetchTopoData(id);
70 expect(promise).toBeDefined(); 70 expect(promise).toBeDefined();
71 expect(promise.meta.wasCached).toBeFalsy(); 71 expect(promise.meta.wasCached).toBeFalsy();
72 expect(promise.tagged).toBeUndefined(); 72 expect(promise.tagged).toBeUndefined();
73 73
74 promise.tagged = 'I woz here'; 74 promise.tagged = 'I woz here';
75 75
76 - promise = gds.fetchGeoMap(id); 76 + promise = gds.fetchTopoData(id);
77 expect(promise).toBeDefined(); 77 expect(promise).toBeDefined();
78 expect(promise.meta.wasCached).toBeTruthy(); 78 expect(promise.meta.wasCached).toBeTruthy();
79 expect(promise.tagged).toEqual('I woz here'); 79 expect(promise.tagged).toEqual('I woz here');
...@@ -81,14 +81,14 @@ describe('factory: fw/svg/geodata.js', function() { ...@@ -81,14 +81,14 @@ describe('factory: fw/svg/geodata.js', function() {
81 81
82 it('should clear the cache when asked', function () { 82 it('should clear the cache when asked', function () {
83 var id = 'foo'; 83 var id = 'foo';
84 - promise = gds.fetchGeoMap(id); 84 + promise = gds.fetchTopoData(id);
85 expect(promise.meta.wasCached).toBeFalsy(); 85 expect(promise.meta.wasCached).toBeFalsy();
86 86
87 - promise = gds.fetchGeoMap(id); 87 + promise = gds.fetchTopoData(id);
88 expect(promise.meta.wasCached).toBeTruthy(); 88 expect(promise.meta.wasCached).toBeTruthy();
89 89
90 gds.clearCache(); 90 gds.clearCache();
91 - promise = gds.fetchGeoMap(id); 91 + promise = gds.fetchTopoData(id);
92 expect(promise.meta.wasCached).toBeFalsy(); 92 expect(promise.meta.wasCached).toBeFalsy();
93 }); 93 });
94 94
...@@ -98,12 +98,64 @@ describe('factory: fw/svg/geodata.js', function() { ...@@ -98,12 +98,64 @@ describe('factory: fw/svg/geodata.js', function() {
98 $httpBackend.expectGET('foo.json').respond(404, 'Not found'); 98 $httpBackend.expectGET('foo.json').respond(404, 'Not found');
99 spyOn($log, 'warn'); 99 spyOn($log, 'warn');
100 100
101 - promise = gds.fetchGeoMap(id); 101 + promise = gds.fetchTopoData(id);
102 $httpBackend.flush(); 102 $httpBackend.flush();
103 - expect(promise.mapdata).toBeUndefined(); 103 + expect(promise.topodata).toBeUndefined();
104 expect($log.warn) 104 expect($log.warn)
105 - .toHaveBeenCalledWith('Failed to retrieve map data: foo.json', 105 + .toHaveBeenCalledWith('Failed to retrieve map TopoJSON data: foo.json',
106 404, 'Not found'); 106 404, 'Not found');
107 }); 107 });
108 108
109 + // --- path generator tests
110 +
111 + function simpleTopology(object) {
112 + return {
113 + type: "Topology",
114 + transform: {scale: [1, 1], translate: [0, 0]},
115 + objects: {states: object},
116 + arcs: [
117 + [[0, 0], [1, 0], [0, 1], [-1, 0], [0, -1]],
118 + [[0, 0], [1, 0], [0, 1]],
119 + [[1, 1], [-1, 0], [0, -1]],
120 + [[1, 1]],
121 + [[0, 0]]
122 + ]
123 + };
124 + }
125 +
126 + function simpleLineStringTopo() {
127 + return simpleTopology({type: "LineString", arcs: [1, 2]});
128 + }
129 +
130 + it('should use default settings if none are supplied', function () {
131 + var gen = gds.createPathGenerator(simpleLineStringTopo());
132 + expect(gen.settings.objectTag).toBe('states');
133 + expect(gen.settings.logicalSize).toBe(1000);
134 + expect(gen.settings.mapFillScale).toBe(.95);
135 + // best we can do for now is test that projection is a function ...
136 + expect(fs.isF(gen.settings.projection)).toBeTruthy();
137 + });
138 +
139 + it('should allow us to override default settings', function () {
140 + var gen = gds.createPathGenerator(simpleLineStringTopo(), {
141 + mapFillScale: .80
142 + });
143 + expect(gen.settings.objectTag).toBe('states');
144 + expect(gen.settings.logicalSize).toBe(1000);
145 + expect(gen.settings.mapFillScale).toBe(.80);
146 + });
147 +
148 + it('should create transformed geodata, and a path generator', function () {
149 + var gen = gds.createPathGenerator(simpleLineStringTopo());
150 + expect(fs.isO(gen.settings)).toBeTruthy();
151 + expect(fs.isO(gen.geodata)).toBeTruthy();
152 + expect(fs.isF(gen.pathgen)).toBeTruthy();
153 + });
154 + // NOTE: we probably should have more unit tests that assert stuff about
155 + // the transformed data (geo data) -- though perhaps we can rely on
156 + // the unit testing of TopoJSON? See...
157 + // https://github.com/mbostock/topojson/blob/master/test/feature-test.js
158 + // and, what about the path generator?, and the computed bounds?
159 + // In summary, more work should be done here..
160 +
109 }); 161 });
......