GUI -- MapService: rework API and internal code for loading map. WIP.
Change-Id: I74458a3ef615d67a0fe9869926ef230990cd902f
Showing
2 changed files
with
143 additions
and
51 deletions
| ... | @@ -42,11 +42,10 @@ | ... | @@ -42,11 +42,10 @@ |
| 42 | 'use strict'; | 42 | 'use strict'; |
| 43 | 43 | ||
| 44 | // injected references | 44 | // injected references |
| 45 | - var $log, $http, $q, fs; | 45 | + var $log, $http, fs; |
| 46 | 46 | ||
| 47 | // internal state | 47 | // internal state |
| 48 | - var maps = d3.map(), | 48 | + var mapCache = d3.map(), |
| 49 | - msgMs = 'MapService.', | ||
| 50 | bundledUrlPrefix = '../data/map/'; | 49 | bundledUrlPrefix = '../data/map/'; |
| 51 | 50 | ||
| 52 | function getUrl(id) { | 51 | function getUrl(id) { |
| ... | @@ -57,56 +56,42 @@ | ... | @@ -57,56 +56,42 @@ |
| 57 | } | 56 | } |
| 58 | 57 | ||
| 59 | angular.module('onosSvg') | 58 | angular.module('onosSvg') |
| 60 | - .factory('MapService', ['$log', '$http', '$q', 'FnService', | 59 | + .factory('MapService', ['$log', '$http', 'FnService', |
| 61 | - | 60 | + function (_$log_, _$http_, _fs_) { |
| 62 | - function (_$log_, _$http_, _$q_, _fs_) { | ||
| 63 | $log = _$log_; | 61 | $log = _$log_; |
| 64 | $http = _$http_; | 62 | $http = _$http_; |
| 65 | - $q = _$q_; | ||
| 66 | fs = _fs_; | 63 | fs = _fs_; |
| 67 | 64 | ||
| 68 | - function clearCache() { | ||
| 69 | - maps = d3.map(); | ||
| 70 | - } | ||
| 71 | - | ||
| 72 | - // NOTE: It is expected that mapLayer is a D3 selection of the | ||
| 73 | - // <g> element (a child of zoomLayer) into which the map | ||
| 74 | - // path data will be rendered. | ||
| 75 | - function renderMap(mapLayer) { | ||
| 76 | - // TODO --- | ||
| 77 | - $log.log('Hey, let\'s render the map...'); | ||
| 78 | - } | ||
| 79 | 65 | ||
| 80 | function fetchGeoMap(id) { | 66 | function fetchGeoMap(id) { |
| 81 | if (!fs.isS(id)) { | 67 | if (!fs.isS(id)) { |
| 82 | return null; | 68 | return null; |
| 83 | } | 69 | } |
| 84 | - var url = getUrl(id); | 70 | + var url = getUrl(id), |
| 85 | - | 71 | + promise = mapCache.get(id); |
| 86 | - var promise = maps.get(id); | ||
| 87 | 72 | ||
| 88 | if (!promise) { | 73 | if (!promise) { |
| 89 | - // need to fetch the data and build the object... | 74 | + // need to fetch the data, build the object, |
| 90 | - var deferred = $q.defer(); | 75 | + // cache it, and return it. |
| 91 | - promise = deferred.promise; | 76 | + promise = $http.get(url); |
| 92 | - | ||
| 93 | - $http.get(url) | ||
| 94 | - .success(function (data) { | ||
| 95 | - deferred.resolve(data); | ||
| 96 | - }) | ||
| 97 | - .error(function (msg, code) { | ||
| 98 | - deferred.reject(msg); | ||
| 99 | - $log.warn(msg, code); | ||
| 100 | - }); | ||
| 101 | 77 | ||
| 102 | promise.meta = { | 78 | promise.meta = { |
| 103 | id: id, | 79 | id: id, |
| 104 | url: url, | 80 | url: url, |
| 105 | - wasCached: false, | 81 | + wasCached: false |
| 106 | - render: renderMap | ||
| 107 | }; | 82 | }; |
| 108 | 83 | ||
| 109 | - maps.set(id, promise); | 84 | + promise.then(function (response) { |
| 85 | + // success | ||
| 86 | + promise.mapdata = response.data; | ||
| 87 | + }, function (response) { | ||
| 88 | + // error | ||
| 89 | + $log.warn('Failed to retrieve map data: ' + url, | ||
| 90 | + response.status, response.data); | ||
| 91 | + }); | ||
| 92 | + | ||
| 93 | + mapCache.set(id, promise); | ||
| 94 | + | ||
| 110 | } else { | 95 | } else { |
| 111 | promise.meta.wasCached = true; | 96 | promise.meta.wasCached = true; |
| 112 | } | 97 | } |
| ... | @@ -114,9 +99,66 @@ | ... | @@ -114,9 +99,66 @@ |
| 114 | return promise; | 99 | return promise; |
| 115 | } | 100 | } |
| 116 | 101 | ||
| 102 | + var geoMapProj; | ||
| 103 | + | ||
| 104 | + function setProjForView(path, topoData) { | ||
| 105 | + var dim = 1000; | ||
| 106 | + | ||
| 107 | + // start with unit scale, no translation.. | ||
| 108 | + geoMapProj.scale(1).translate([0, 0]); | ||
| 109 | + | ||
| 110 | + // figure out dimensions of map data.. | ||
| 111 | + var b = path.bounds(topoData), | ||
| 112 | + x1 = b[0][0], | ||
| 113 | + y1 = b[0][1], | ||
| 114 | + x2 = b[1][0], | ||
| 115 | + y2 = b[1][1], | ||
| 116 | + dx = x2 - x1, | ||
| 117 | + dy = y2 - y1, | ||
| 118 | + x = (x1 + x2) / 2, | ||
| 119 | + y = (y1 + y2) / 2; | ||
| 120 | + | ||
| 121 | + // size map to 95% of minimum dimension to fill space.. | ||
| 122 | + var s = .95 / Math.min(dx / dim, dy / dim); | ||
| 123 | + var t = [dim / 2 - s * x, dim / 2 - s * y]; | ||
| 124 | + | ||
| 125 | + // set new scale, translation on the projection.. | ||
| 126 | + geoMapProj.scale(s).translate(t); | ||
| 127 | + } | ||
| 128 | + | ||
| 129 | + | ||
| 130 | + function loadMapInto(mapLayer, id) { | ||
| 131 | + var mapObject = fetchGeoMap(id); | ||
| 132 | + if (!mapObject) { | ||
| 133 | + $log.warn('Failed to load map: ' + id); | ||
| 134 | + return null; | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + var mapdata = mapObject.mapdata, | ||
| 138 | + topoData, path; | ||
| 139 | + | ||
| 140 | + mapObject.then(function () { | ||
| 141 | + // extracts the topojson data into geocoordinate-based geometry | ||
| 142 | + topoData = topojson.feature(mapdata, mapdata.objects.states); | ||
| 143 | + | ||
| 144 | + // see: http://bl.ocks.org/mbostock/4707858 | ||
| 145 | + geoMapProj = d3.geo.mercator(); | ||
| 146 | + path = d3.geo.path().projection(geoMapProj); | ||
| 147 | + | ||
| 148 | + setProjForView(path, topoData); | ||
| 149 | + | ||
| 150 | + mapLayer.selectAll('path') | ||
| 151 | + .data(topoData.features) | ||
| 152 | + .enter() | ||
| 153 | + .append('path') | ||
| 154 | + .attr('d', path); | ||
| 155 | + }); | ||
| 156 | + // TODO: review whether we should just return true (not the map object) | ||
| 157 | + return mapObject; | ||
| 158 | + } | ||
| 159 | + | ||
| 117 | return { | 160 | return { |
| 118 | - clearCache: clearCache, | 161 | + loadMapInto: loadMapInto |
| 119 | - fetchGeoMap: fetchGeoMap | ||
| 120 | }; | 162 | }; |
| 121 | }]); | 163 | }]); |
| 122 | 164 | ... | ... |
| ... | @@ -20,20 +20,21 @@ | ... | @@ -20,20 +20,21 @@ |
| 20 | @author Simon Hunt | 20 | @author Simon Hunt |
| 21 | */ | 21 | */ |
| 22 | describe('factory: fw/svg/map.js', function() { | 22 | describe('factory: fw/svg/map.js', function() { |
| 23 | - var $log, fs, ms, d3Elem, promise; | 23 | + var $log, $httpBackend, fs, ms, d3Elem, promise; |
| 24 | 24 | ||
| 25 | beforeEach(module('onosUtil', 'onosSvg')); | 25 | beforeEach(module('onosUtil', 'onosSvg')); |
| 26 | 26 | ||
| 27 | - beforeEach(inject(function (_$log_, FnService, MapService) { | 27 | + beforeEach(inject(function (_$log_, _$httpBackend_, FnService, MapService) { |
| 28 | $log = _$log_; | 28 | $log = _$log_; |
| 29 | + $httpBackend = _$httpBackend_; | ||
| 29 | fs = FnService; | 30 | fs = FnService; |
| 30 | ms = MapService; | 31 | ms = MapService; |
| 31 | - ms.clearCache(); | 32 | + //ms.clearCache(); |
| 32 | - // TODO: d3Elem = d3.select('body').append('...').attr('id', 'myFoo'); | 33 | + d3Elem = d3.select('body').append('svg').append('g').attr('id', 'mapLayer'); |
| 33 | })); | 34 | })); |
| 34 | 35 | ||
| 35 | afterEach(function () { | 36 | afterEach(function () { |
| 36 | - // TODO d3.select('#myFoo').remove(); | 37 | + d3.select('svg').remove(); |
| 37 | }); | 38 | }); |
| 38 | 39 | ||
| 39 | it('should define MapService', function () { | 40 | it('should define MapService', function () { |
| ... | @@ -42,10 +43,54 @@ describe('factory: fw/svg/map.js', function() { | ... | @@ -42,10 +43,54 @@ describe('factory: fw/svg/map.js', function() { |
| 42 | 43 | ||
| 43 | it('should define api functions', function () { | 44 | it('should define api functions', function () { |
| 44 | expect(fs.areFunctions(ms, [ | 45 | expect(fs.areFunctions(ms, [ |
| 45 | - 'clearCache', 'fetchGeoMap' | 46 | + 'loadMapInto' |
| 46 | ])).toBeTruthy(); | 47 | ])).toBeTruthy(); |
| 47 | }); | 48 | }); |
| 48 | 49 | ||
| 50 | + var fakeMapId = '../tests/app/fw/svg/fake-map-data', | ||
| 51 | + fakeMapUrl = fakeMapId + '.json'; | ||
| 52 | + | ||
| 53 | + var fakeMapData = { | ||
| 54 | + "type": "Topology", | ||
| 55 | + "objects": { | ||
| 56 | + "states": { | ||
| 57 | + "type": "GeometryCollection", | ||
| 58 | + "geometries": [ | ||
| 59 | + { "type": "Polygon", "arcs": [[0, 1]]}, | ||
| 60 | + { "type": "Polygon", "arcs": [[2, 3]]} | ||
| 61 | + ] | ||
| 62 | + } | ||
| 63 | + }, | ||
| 64 | + "arcs": [ | ||
| 65 | + [ [6347, 2300], [ -16, -9], [ -22, 1], [ -5, 3], [ 9, 6], [ 27, 7], [ 7, -8]], | ||
| 66 | + [ [6447, 2350], [ -4, -4], [ -19, -41], [ -66, -14], [ 4, 9], [ 14, 2]], | ||
| 67 | + [ [6290, 2347], [ -2, 83], [ -2, 76], [ -2, 75], [ -2, 76], [ -2, 76], [ -2, 75]], | ||
| 68 | + [ [6329, 4211], [ -3, 6], [ -2, 4], [ 2, 1], [ 28, -1], [ 28, 0]] | ||
| 69 | + ], | ||
| 70 | + "transform": { | ||
| 71 | + "scale": [0.005772872856602365, 0.0024829805705001468], | ||
| 72 | + "translate": [-124.70997774915153, 24.542349340056283] | ||
| 73 | + } | ||
| 74 | + }; | ||
| 75 | + | ||
| 76 | + | ||
| 77 | + it('should load map into layer', function () { | ||
| 78 | + $httpBackend.expectGET(fakeMapUrl).respond(fakeMapData); | ||
| 79 | + | ||
| 80 | + var obj = ms.loadMapInto(d3Elem, fakeMapId); | ||
| 81 | + //$httpBackend.flush(); | ||
| 82 | + // TODO: figure out how to test this function as a black box test. | ||
| 83 | + | ||
| 84 | + expect(obj).toBeTruthy(); | ||
| 85 | + debugger; | ||
| 86 | + | ||
| 87 | + // todo: assert that paths are added to map layer element | ||
| 88 | + }); | ||
| 89 | + | ||
| 90 | +/* | ||
| 91 | + | ||
| 92 | + | ||
| 93 | + | ||
| 49 | it('should return null when no parameters given', function () { | 94 | it('should return null when no parameters given', function () { |
| 50 | promise = ms.fetchGeoMap(); | 95 | promise = ms.fetchGeoMap(); |
| 51 | expect(promise).toBeNull(); | 96 | expect(promise).toBeNull(); |
| ... | @@ -95,13 +140,18 @@ describe('factory: fw/svg/map.js', function() { | ... | @@ -95,13 +140,18 @@ describe('factory: fw/svg/map.js', function() { |
| 95 | expect(promise.meta.wasCached).toBeFalsy(); | 140 | expect(promise.meta.wasCached).toBeFalsy(); |
| 96 | }); | 141 | }); |
| 97 | 142 | ||
| 98 | - it('should load USA into cache', function () { | 143 | + |
| 99 | - var id = '*continental_us'; | 144 | + it('should log a warning if data fails to load', function () { |
| 100 | - promise = ms.fetchGeoMap(id); | 145 | + $httpBackend.expectGET(mapurl).respond(404, 'Not found'); |
| 101 | - expect(promise).toBeDefined(); | 146 | + spyOn($log, 'warn'); |
| 102 | - expect(promise.meta.id).toBe(id); | 147 | + |
| 103 | - expect(promise.meta.url).toBe('../data/map/continental_us.json'); | 148 | + promise = ms.fetchGeoMap(mapid); |
| 104 | - // TODO: WIP -- after a pause, the data should be there!!! | 149 | + $httpBackend.flush(); |
| 150 | + expect(promise.mapdata).toBeUndefined(); | ||
| 151 | + expect($log.warn) | ||
| 152 | + .toHaveBeenCalledWith('Failed to retrieve map data: ' + mapurl, | ||
| 153 | + 404, 'Not found'); | ||
| 105 | 154 | ||
| 106 | }); | 155 | }); |
| 156 | +*/ | ||
| 107 | }); | 157 | }); | ... | ... |
-
Please register or login to post a comment