diff --git a/app/controllers/app.js b/app/controllers/app.js index f992fba9..d72641fe 100755 --- a/app/controllers/app.js +++ b/app/controllers/app.js @@ -79,7 +79,7 @@ function sanitize_filename(filename) { // request handlers 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_height = 768.0; @@ -187,7 +187,7 @@ function handleQuery(req, res) { } // 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(""); } else if (format === 'shp') { return null; @@ -255,7 +255,9 @@ function handleQuery(req, res) { // TODO: refactor formats to external object if (format === 'geojson'){ - toGeoJSON(result, res, this); + toGeoJSON(result, gn, this); + } else if (format === 'topojson'){ + toTopoJSON(result, gn, this); } else if (format === 'svg'){ toSVG(result.rows, gn, this); } else if (format === 'csv'){ @@ -299,7 +301,7 @@ function handleCacheStatus(req, res){ } // helper functions -function toGeoJSON(data, res, callback){ +function toGeoJSON(data, gn, callback){ try{ var out = { type: "FeatureCollection", @@ -312,9 +314,9 @@ function toGeoJSON(data, res, callback){ properties: { }, geometry: { } }; - geojson.geometry = JSON.parse(ele["the_geom"]); - delete ele["the_geom"]; - delete ele["the_geom_webmercator"]; + geojson.geometry = JSON.parse(ele[gn]); + delete ele[gn]; + delete ele["the_geom_webmercator"]; // TODO: use skipfields geojson.properties = ele; 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){ var radius = 5; // in pixels (based on svg_width and svg_height) @@ -734,6 +749,9 @@ function getContentDisposition(format, filename, inline) { if (format === 'geojson'){ ext = 'geojson'; } + else if (format === 'topojson'){ + ext = 'topojson'; + } else if (format === 'csv'){ ext = 'csv'; } diff --git a/doc/API.md b/doc/API.md index d61198a4..7c24b650 100644 --- a/doc/API.md +++ b/doc/API.md @@ -12,7 +12,7 @@ Supported query string parameters: 'format': Specifies which format to use for the response. Supported formats: JSON (the default), GeoJSON, - CSV, SVG, SHP + TopoJSON, CSV, SVG, SHP 'filename': Sets the filename to use for the query result file attachment @@ -22,7 +22,7 @@ Supported query string parameters: in output. Only useful with "SELECT *" queries. 'dp': Number of digits after the decimal point. - Only affects format=GeoJSON. + Only affects format GeoJSON, TopoJSON, SVG. By default this is 6. '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 --------------- diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 2851da0c..3b29991e 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -186,12 +186,7 @@ }, "pg": { "version": "0.8.7-cdb1", - "from": "git://github.com/CartoDB/node-postgres.git#cdb_production", - "dependencies": { - "generic-pool": { - "version": "1.0.9" - } - } + "from": "git://github.com/CartoDB/node-postgres.git#cdb_production" }, "generic-pool": { "version": "1.0.12" @@ -205,6 +200,19 @@ "step": { "version": "0.0.5" }, + "topojson": { + "version": "0.0.8", + "dependencies": { + "optimist": { + "version": "0.3.5", + "dependencies": { + "wordwrap": { + "version": "0.0.2" + } + } + } + } + }, "oauth-client": { "version": "0.2.0", "dependencies": { diff --git a/package.json b/package.json index 7012f6fd..04b2a625 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "redis": "0.7.1", "hiredis": "*", "step": "0.0.x", + "topojson": "~0.0.8", "oauth-client": "0.2.0", "node-uuid":"1.3.3", "strftime":"~0.4.7", @@ -29,7 +30,7 @@ "libxmljs": "~0.6.1" }, "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" } } diff --git a/test/acceptance/export/topojson.js b/test/acceptance/export/topojson.js new file mode 100644 index 00000000..4dcd0796 --- /dev/null +++ b/test/acceptance/export/topojson.js @@ -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(); + }); +}); + +});