Initial support for TopoJSON (#79)

Does not include any attributes in the format
This commit is contained in:
Sandro Santilli 2013-01-09 17:43:23 +01:00
parent e311c388b6
commit 39669578b6
5 changed files with 157 additions and 17 deletions

View File

@ -79,7 +79,7 @@ function sanitize_filename(filename) {
// request handlers // request handlers
function handleQuery(req, res) { function handleQuery(req, res) {
var supportedFormats = ['json', 'geojson', 'csv', 'svg', 'shp', 'kml']; var supportedFormats = ['json', 'geojson', 'topojson', 'csv', 'svg', 'shp', 'kml'];
var svg_width = 1024.0; var svg_width = 1024.0;
var svg_height = 768.0; var svg_height = 768.0;
@ -187,7 +187,7 @@ function handleQuery(req, res) {
} }
// TODO: refactor formats to external object // TODO: refactor formats to external object
if (format === 'geojson'){ if (format === 'geojson' || format === 'topojson' ){
sql = ['SELECT *, ST_AsGeoJSON(the_geom,',dp,') as the_geom FROM (', sql, ') as foo'].join(""); sql = ['SELECT *, ST_AsGeoJSON(the_geom,',dp,') as the_geom FROM (', sql, ') as foo'].join("");
} else if (format === 'shp') { } else if (format === 'shp') {
return null; return null;
@ -255,7 +255,9 @@ function handleQuery(req, res) {
// TODO: refactor formats to external object // TODO: refactor formats to external object
if (format === 'geojson'){ if (format === 'geojson'){
toGeoJSON(result, res, this); toGeoJSON(result, gn, this);
} else if (format === 'topojson'){
toTopoJSON(result, gn, this);
} else if (format === 'svg'){ } else if (format === 'svg'){
toSVG(result.rows, gn, this); toSVG(result.rows, gn, this);
} else if (format === 'csv'){ } else if (format === 'csv'){
@ -299,7 +301,7 @@ function handleCacheStatus(req, res){
} }
// helper functions // helper functions
function toGeoJSON(data, res, callback){ function toGeoJSON(data, gn, callback){
try{ try{
var out = { var out = {
type: "FeatureCollection", type: "FeatureCollection",
@ -312,9 +314,9 @@ function toGeoJSON(data, res, callback){
properties: { }, properties: { },
geometry: { } geometry: { }
}; };
geojson.geometry = JSON.parse(ele["the_geom"]); geojson.geometry = JSON.parse(ele[gn]);
delete ele["the_geom"]; delete ele[gn];
delete ele["the_geom_webmercator"]; delete ele["the_geom_webmercator"]; // TODO: use skipfields
geojson.properties = ele; geojson.properties = ele;
out.features.push(geojson); out.features.push(geojson);
}); });
@ -326,6 +328,19 @@ function toGeoJSON(data, res, callback){
} }
} }
function toTopoJSON(data, gn, callback){
toGeoJSON(data, gn, function(err, geojson) {
if ( err ) {
callback(err, null);
return;
}
var TopoJSON = require('topojson');
// TODO: provide some identifiers here
var topology = TopoJSON.topology(geojson.features);
callback(err, topology);
});
}
function toSVG(rows, gn, callback){ function toSVG(rows, gn, callback){
var radius = 5; // in pixels (based on svg_width and svg_height) var radius = 5; // in pixels (based on svg_width and svg_height)
@ -734,6 +749,9 @@ function getContentDisposition(format, filename, inline) {
if (format === 'geojson'){ if (format === 'geojson'){
ext = 'geojson'; ext = 'geojson';
} }
else if (format === 'topojson'){
ext = 'topojson';
}
else if (format === 'csv'){ else if (format === 'csv'){
ext = 'csv'; ext = 'csv';
} }

View File

@ -12,7 +12,7 @@ Supported query string parameters:
'format': Specifies which format to use for the response. 'format': Specifies which format to use for the response.
Supported formats: JSON (the default), GeoJSON, Supported formats: JSON (the default), GeoJSON,
CSV, SVG, SHP TopoJSON, CSV, SVG, SHP
'filename': Sets the filename to use for the query result 'filename': Sets the filename to use for the query result
file attachment file attachment
@ -22,7 +22,7 @@ Supported query string parameters:
in output. Only useful with "SELECT *" queries. in output. Only useful with "SELECT *" queries.
'dp': Number of digits after the decimal point. 'dp': Number of digits after the decimal point.
Only affects format=GeoJSON. Only affects format GeoJSON, TopoJSON, SVG.
By default this is 6. By default this is 6.
'api_key': Needed to authenticate in order to modify the database. 'api_key': Needed to authenticate in order to modify the database.
@ -88,7 +88,7 @@ The GeoJSON response is follows:
} }
``` ```
TODO: csv, kml responses TODO: csv, kml, svg, topojson responses
Response errors Response errors
--------------- ---------------

20
npm-shrinkwrap.json generated
View File

@ -186,12 +186,7 @@
}, },
"pg": { "pg": {
"version": "0.8.7-cdb1", "version": "0.8.7-cdb1",
"from": "git://github.com/CartoDB/node-postgres.git#cdb_production", "from": "git://github.com/CartoDB/node-postgres.git#cdb_production"
"dependencies": {
"generic-pool": {
"version": "1.0.9"
}
}
}, },
"generic-pool": { "generic-pool": {
"version": "1.0.12" "version": "1.0.12"
@ -205,6 +200,19 @@
"step": { "step": {
"version": "0.0.5" "version": "0.0.5"
}, },
"topojson": {
"version": "0.0.8",
"dependencies": {
"optimist": {
"version": "0.3.5",
"dependencies": {
"wordwrap": {
"version": "0.0.2"
}
}
}
}
},
"oauth-client": { "oauth-client": {
"version": "0.2.0", "version": "0.2.0",
"dependencies": { "dependencies": {

View File

@ -18,6 +18,7 @@
"redis": "0.7.1", "redis": "0.7.1",
"hiredis": "*", "hiredis": "*",
"step": "0.0.x", "step": "0.0.x",
"topojson": "~0.0.8",
"oauth-client": "0.2.0", "oauth-client": "0.2.0",
"node-uuid":"1.3.3", "node-uuid":"1.3.3",
"strftime":"~0.4.7", "strftime":"~0.4.7",
@ -29,7 +30,7 @@
"libxmljs": "~0.6.1" "libxmljs": "~0.6.1"
}, },
"scripts": { "scripts": {
"test": "test/run_tests.sh ${RUNTESTFLAGS} test/unit/redis_pool.test.js test/unit/metadata.test.js test/unit/oauth.test.js test/unit/psql.test.js test/acceptance/app.test.js test/acceptance/app.auth.test.js" "test": "test/run_tests.sh ${RUNTESTFLAGS} test/unit/redis_pool.test.js test/unit/metadata.test.js test/unit/oauth.test.js test/unit/psql.test.js test/acceptance/app.test.js test/acceptance/app.auth.test.js test/acceptance/export/topojson.js"
}, },
"engines": { "node": ">= 0.4.1 < 0.9" } "engines": { "node": ">= 0.4.1 < 0.9" }
} }

View File

@ -0,0 +1,113 @@
require('../../helper');
require('../../support/assert');
var app = require(global.settings.app_root + '/app/controllers/app')
, assert = require('assert')
, querystring = require('querystring')
, _ = require('underscore')
, zipfile = require('zipfile')
, fs = require('fs')
, libxmljs = require('libxmljs')
;
// allow lots of emitters to be set to silence warning
app.setMaxListeners(0);
suite('export.topojson', function() {
// TOPOJSON tests
test('GET two polygons sharing an edge as topojson', function(done){
assert.response(app, {
url: '/api/v1/sql?' + querystring.stringify({
q: "SELECT 1 as cartodb_id, 'POLYGON((-5 0,5 0,0 5,-5 0))'::geometry as the_geom " +
" UNION ALL " +
"SELECT 2 as cartodb_id, 'POLYGON((0 -5,0 5,-5 0,0 -5))'::geometry as the_geom ",
format: 'topojson'
}),
headers: {host: 'vizzuality.cartodb.com'},
method: 'GET'
},{ }, function(res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
assert.equal(true, /^attachment/.test(cd), 'TOPOJSON is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.topojson/gi.test(cd));
var topojson = JSON.parse(res.body);
assert.equal(topojson.type, 'Topology');
// Check transform
assert.ok(topojson.hasOwnProperty('transform'));
var trans = topojson.transform;
assert.equal(_.keys(trans).length, 2); // only scale and translate
assert.equal(trans.scale.length, 2); // scalex, scaley
assert.equal(Math.round(trans.scale[0]*1e6), 1000);
assert.equal(Math.round(trans.scale[1]*1e6), 1000);
assert.equal(trans.translate.length, 2); // translatex, translatey
assert.equal(trans.translate[0], -5);
assert.equal(trans.translate[1], -5);
// Check objects
assert.ok(topojson.hasOwnProperty('objects'));
assert.equal(_.keys(topojson.objects).length, 2);
var obj = topojson.objects[0];
//console.dir(obj);
// Expected: { type: 'Polygon', arcs: [ [ 0, 1 ] ] }
assert.equal(_.keys(obj).length, 2); // only type and arcs, no props
assert.equal(obj.type, 'Polygon');
assert.equal(obj.arcs.length, 1); /* only shell, no holes */
var shell = obj.arcs[0];
assert.equal(shell.length, 2); /* one shared arc, one non-shared */
assert.equal(shell[0], 0); /* shared arc */
assert.equal(shell[1], 1); /* non-shared arc */
obj = topojson.objects[1];
//console.dir(obj);
// Expected: { type: 'Polygon', arcs: [ [ 0, 2 ] ] }
assert.equal(_.keys(obj).length, 2); // only type and arcs, no props
assert.equal(obj.type, 'Polygon');
assert.equal(obj.arcs.length, 1); /* only shell, no holes */
shell = obj.arcs[0];
assert.equal(shell.length, 2); /* one shared arc, one non-shared */
assert.equal(shell[0], 0); /* shared arc */
assert.equal(shell[1], 2); /* non-shared arc */
// Check arcs
assert.ok(topojson.hasOwnProperty('arcs'));
assert.equal(topojson.arcs.length, 3); // one shared, two non-shared
var arc = topojson.arcs[0]; // shared arc
assert.equal(arc.length, 2); // shared arc has two vertices
var p = arc[0];
assert.equal(Math.round(p[0]*trans.scale[0]), 0);
assert.equal(Math.round(p[1]*trans.scale[1]), 5);
p = arc[1];
assert.equal(Math.round(p[0]*trans.scale[0]), 5);
assert.equal(Math.round(p[1]*trans.scale[1]), 5);
arc = topojson.arcs[1]; // non shared arc
assert.equal(arc.length, 3); // non shared arcs have three vertices
p = arc[0];
assert.equal(Math.round(p[0]*trans.scale[0]), 5);
assert.equal(Math.round(p[1]*trans.scale[1]), 10);
p = arc[1];
assert.equal(Math.round(p[0]*trans.scale[0]), 5);
assert.equal(Math.round(p[1]*trans.scale[1]), -5);
p = arc[2];
assert.equal(Math.round(p[0]*trans.scale[0]), -10);
assert.equal(Math.round(p[1]*trans.scale[1]), 0);
arc = topojson.arcs[2]; // non shared arc
assert.equal(arc.length, 3); // non shared arcs have three vertices
p = arc[0];
assert.equal(Math.round(p[0]*trans.scale[0]), 5);
assert.equal(Math.round(p[1]*trans.scale[1]), 10);
p = arc[1];
assert.equal(Math.round(p[0]*trans.scale[0]), 0);
assert.equal(Math.round(p[1]*trans.scale[1]), -10);
p = arc[2];
assert.equal(Math.round(p[0]*trans.scale[0]), -5);
assert.equal(Math.round(p[1]*trans.scale[1]), 5);
done();
});
});
});