Initial support for TopoJSON (#79)
Does not include any attributes in the format
This commit is contained in:
parent
e311c388b6
commit
39669578b6
@ -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';
|
||||||
}
|
}
|
||||||
|
@ -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
20
npm-shrinkwrap.json
generated
@ -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": {
|
||||||
|
@ -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" }
|
||||||
}
|
}
|
||||||
|
113
test/acceptance/export/topojson.js
Normal file
113
test/acceptance/export/topojson.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user