GUI -- Further work on MapService and GeoDataService. Still WIP.
Change-Id: I92e826cc15cc1a07238cc4b4eac20583260a3c84
Showing
4 changed files
with
152 additions
and
136 deletions
... | @@ -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 | }); | ... | ... |
-
Please register or login to post a comment