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..
+
});