├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── lib └── dbgeo.js ├── package.json └── test ├── README.md ├── credentials.js.example ├── setup.sh └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.dat 5 | *.out 6 | *.pid 7 | *.gz 8 | 9 | npm-debug.log 10 | node_modules 11 | test/node_modules 12 | test/credentials.js 13 | 14 | .DS_Store 15 | .AppleDouble 16 | .LSOverride 17 | Icon 18 | 19 | # Files that might appear on external disk 20 | .Spotlight-V100 21 | .Trashes 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 4 | + Coordinate pairs are now specified as `longitude, latitude` instead of `latitude, longitude` 5 | + `.parse(...)` now has three parameters instead of two - `data`, `options`, and `callback`. `data` is identical to `params.data`, just in a different place, and `options` is now optional 6 | + New default options: 7 | 8 | 9 | | option | old | new | 10 | | --------------- |:-------------:| -------| 11 | | geometryType | geojson | wkb | 12 | | geometryColumn | geometry | geom | 13 | | outputFormat | geojson | geojson | 14 | 15 | 16 | + Integratation of [geojson-precision](https://github.com/jczaplew/geojson-precision). Can be used by specifying a `precision` in `options` 17 | 18 | + Readablilty, style, and performance enhancements 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dbgeo 2 | 3 | Convert database query results to GeoJSON or TopoJSON. Inspired by [Bryan McBride's](https://github.com/bmcbride) [PHP-Database-GeoJSON](https://github.com/bmcbride/PHP-Database-GeoJSON). Works with your database of choice - ideally paired with [node-mysql](https://github.com/felixge/node-mysql), [node-postgres](https://github.com/brianc/node-postgres), or [mongodb](https://github.com/mongodb/node-mongodb-native). It is a more flexible version of [postgeo](https://github.com/jczaplew/postgeo) and [mysql2geojson](https://github.com/jczaplew/mysql2geojson) (*both deprecated*). 4 | 5 | ###### Installation 6 | ```` 7 | npm install dbgeo 8 | ```` 9 | 10 | ###### Example Usage 11 | ````javascript 12 | var dbgeo = require('dbgeo') 13 | 14 | // Query a database... 15 | 16 | dbgeo.parse(data, { 17 | outputFormat: 'geojson' 18 | }, function(error, result) { 19 | // This will log a valid GeoJSON FeatureCollection 20 | console.log(result) 21 | }); 22 | 23 | ```` 24 | 25 | See ````test/test.js```` for more examples. 26 | 27 | 28 | ## API 29 | 30 | ### .parse(data, options, callback) 31 | 32 | ##### data (***required***) 33 | An array of objects, usually results from a database query. 34 | 35 | ##### options (*optional*) 36 | Configuration object that can contain the following keys: 37 | 38 | | argument | description | values | default value | 39 | |----------|---------------|---------|-----------------| 40 | | `geometryType` | Format of input geometry | wkb, wkt, geojson, ll | wkb | 41 | | `geometryColumn`| Name of column that contains geometry. If input geometry type is "ll", this is an array in the format ````['longitude', 'latitude']```` | *Any string* | geom | 42 | | `outputFormat` | Desired output format | geojson, topojson | geojson | 43 | | `precision` | Trim the coordinate precision of the output to a given number of digits using [geojson-precision](https://github.com/jczaplew/geojson-precision) | *Any integer* | `null` (will not trim precision) | 44 | | `quantization` | Value for quantization process, typically specified as powers of ten, see [topojson.quantize](https://github.com/topojson/topojson-client/blob/master/README.md#quantize) | *Any integer greater than one* | `null` (no quantization) | 45 | 46 | 47 | ##### callback (***required***) 48 | A function with two parameters: an error, and a result object. 49 | 50 | Examples can be found in ````test/test.js````. 51 | 52 | ### .defaults{} 53 | The default options for ````.parse()````. You can set these before using ````.parse()```` if you plan to use the same options continuously. 54 | 55 | ## License 56 | CC0 57 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/dbgeo'); -------------------------------------------------------------------------------- /lib/dbgeo.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var topojson = require('topojson') 3 | var wkx = require('wkx') 4 | var gp = require('geojson-precision') 5 | 6 | var dbgeo = {} 7 | 8 | dbgeo.defaults = { 9 | outputFormat: 'geojson', 10 | geometryColumn: 'geom', 11 | geometryType: 'wkb', 12 | precision: null, 13 | quantization: null 14 | } 15 | 16 | dbgeo.parse = function(data, params, callback) { 17 | // Validate and parse inputs 18 | if (!data) { 19 | return callback('You must provide a value for "data"') 20 | } 21 | 22 | if (!callback) { 23 | throw new Error('You must provide a callback function') 24 | } 25 | 26 | params = params || {} 27 | 28 | params.geometryColumn = (params && params.geometryColumn) ? params.geometryColumn : this.defaults.geometryColumn 29 | params.geometryType = (params && params.geometryType) ? params.geometryType : this.defaults.geometryType 30 | params.outputFormat = (params && params.outputFormat) ? params.outputFormat : this.defaults.outputFormat 31 | params.precision = (params && params.precision) ? params.precision : this.defaults.precision 32 | params.quantization = (params && params.quantization) ? params.quantization : this.defaults.quantization 33 | 34 | if (['geojson', 'topojson'].indexOf(params.outputFormat) < 0) { 35 | return callback('Invalid outputFormat value. Please use either "geojson" or "topojson"') 36 | } 37 | 38 | if (['wkt', 'geojson', 'll', 'wkb'].indexOf(params.geometryType) < 0) { 39 | return callback('Invalid geometry type. Please use "wkt", "wkb", "geojson", or "ll"') 40 | } 41 | 42 | if (params.geometryType === 'll' && (!Array.isArray(params.geometryColumn) || params.geometryColumn.length !== 2)) { 43 | return callback('When the input data type is lng/lat, please specify the "geometryColumn" as an array with two parameters, the longitude followed by the latitude') 44 | } 45 | 46 | // Convert to GeoJSON 47 | var output = { 48 | type: 'FeatureCollection', 49 | features: data.map(function(row) { 50 | return { 51 | type: 'Feature', 52 | geometry: function (type, geom) { 53 | if (!geom) { 54 | return null 55 | } 56 | switch(type) { 57 | case 'wkb': 58 | return wkx.Geometry.parse(new Buffer(geom, 'hex')).toGeoJSON() 59 | case 'geojson': 60 | return JSON.parse(geom) 61 | case 'wkt': 62 | return wkx.Geometry.parse(geom).toGeoJSON() 63 | case 'll': 64 | return new wkx.Point(geom[0], geom[1]).toGeoJSON() 65 | default: 66 | return null 67 | } 68 | }(params.geometryType, (params.geometryType === 'll' ? [ row[params.geometryColumn[0]], row[params.geometryColumn[1]] ] : row[params.geometryColumn])), 69 | properties: function (geomColumn, props) { 70 | var properties = {} 71 | 72 | Object.keys(props).filter(function(d) { 73 | if (d !== geomColumn && geomColumn.indexOf(d) === -1) return d 74 | }).forEach(function(d) { 75 | properties[d] = props[d] 76 | }) 77 | 78 | return properties 79 | }(params.geometryColumn, row) 80 | } 81 | }) 82 | } 83 | 84 | // Trim coordinate precision, if specified 85 | if (params.precision) { 86 | output = gp(output, params.precision) 87 | } 88 | 89 | // Convert to topojson, if needed 90 | if (params.outputFormat === 'topojson') { 91 | callback(null, 92 | topojson.topology({ 93 | output: output 94 | }, { 95 | 'property-transform': function(feature) { 96 | return feature.properties 97 | }, 98 | quantization: params.quantization 99 | }) 100 | ) 101 | } else { 102 | callback(null, output) 103 | } 104 | } 105 | 106 | module.exports = dbgeo 107 | }()) 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dbgeo", 3 | "version": "1.1.0", 4 | "description": "A Node.js module for converting database queries to GeoJSON and TopoJSON", 5 | "scripts": { 6 | "test": "node test/test.js" 7 | }, 8 | "keywords": [ 9 | "TopoJSON", 10 | "GeoJSON", 11 | "PostGIS", 12 | "MySQL", 13 | "Postgres", 14 | "PostgreSQL", 15 | "WKT", 16 | "WKB", 17 | "MariaDB", 18 | "dbgeo", 19 | "geometry", 20 | "GIS", 21 | "mysql2geojson", 22 | "postgeo" 23 | ], 24 | "main": "./dbgeo", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/jczaplew/dbgeo.git" 28 | }, 29 | "author": "John J Czaplewski", 30 | "license": "CC0", 31 | "dependencies": { 32 | "geojson-precision": "^0.4.0", 33 | "topojson": "~1.6.26", 34 | "wkx": "~0.2.0" 35 | }, 36 | "devDependencies": { 37 | "async": "~2.0.0", 38 | "geojsonhint": "^2.0.0-beta", 39 | "pg": "^6.0.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests/Examples 2 | 3 | ### Dependencies 4 | - Postgres 9+ 5 | 6 | ### Setup 7 | 1. Edit ````setup.sh```` with your Postgres user name 8 | 2. Run ````./setup.sh```` 9 | 3. Edit ````credentials.js.example```` with your Postgres credentials, and rename to ````credentials.js```` 10 | -------------------------------------------------------------------------------- /test/credentials.js.example: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | user: 'you', 3 | host: 'localhost', 4 | port: 5432, 5 | "database": "dbgeo" 6 | } 7 | -------------------------------------------------------------------------------- /test/setup.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | export DB_USER=you 4 | 5 | curl -LOk http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip -o "ne_110m_admin_0_countries.zip" 6 | 7 | curl -LOk http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_populated_places.zip -o "ne_110m_populated_places.zip" 8 | 9 | unzip ne_110m_admin_0_countries.zip 10 | 11 | unzip ne_110m_populated_places.zip 12 | 13 | rm ne_110m_admin_0_countries.zip 14 | rm ne_110m_populated_places.zip 15 | 16 | createdb dbgeo 17 | 18 | psql -U $DB_USER dbgeo -c "CREATE EXTENSION postgis;" 19 | psql -U $DB_USER dbgeo -c "CREATE EXTENSION postgis_topology;" 20 | 21 | shp2pgsql -s 4326 -W "latin1" ne_110m_admin_0_countries.shp public.countries | psql -h localhost -U $DB_USER -d dbgeo -p 5432 22 | 23 | shp2pgsql -s 4326 -W "latin1" ne_110m_populated_places.shp public.places | psql -h localhost -U $DB_USER -d dbgeo -p 5432 24 | 25 | rm ne_* 26 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var pg = require('pg') 2 | var credentials = require('./credentials') 3 | var dbgeo = require('../lib/dbgeo') 4 | var series = require('async/series') 5 | var geojsonhint = require('geojsonhint') 6 | var client = new pg.Client('postgres://' + credentials.user + '@' + credentials.host + ':' + credentials.port + '/' + credentials.database) 7 | 8 | // Connect to postgres 9 | client.connect(function(error, success) { 10 | if (error) { 11 | console.log('Could not connect to postgres') 12 | } 13 | }) 14 | 15 | series([ 16 | function(callback) { 17 | console.time('test1') 18 | /* Select two points, returning the geometry as a GeoJSON object in the field 'geometry' */ 19 | client.query('SELECT name, ST_AsGeoJSON(geom, 6) AS geometry FROM places LIMIT 2', function(error, result) { 20 | dbgeo.parse(result.rows, { 21 | geometryType: 'geojson', 22 | geometryColumn: 'geometry' 23 | }, function(error, result) { 24 | console.timeEnd('test1') 25 | if (error) { 26 | callback({test: 1, error: error }) 27 | } else { 28 | var errors = geojsonhint.hint(result) 29 | if (errors.length) { 30 | callback({test: 1, error: errors}) 31 | } else { 32 | callback(null) 33 | } 34 | 35 | //console.log(JSON.stringify(result)); 36 | } 37 | }) 38 | }) 39 | }, 40 | function(callback) { 41 | console.time('test2') 42 | /* Select two polygons, returning the geometry as a GeoJSON object in the field 'geom', then 43 | converting the result to TopoJSON */ 44 | client.query('SELECT name, ST_AsGeoJSON(geom, 6) AS geom FROM countries LIMIT 2', function(error, result) { 45 | dbgeo.parse(result.rows, { 46 | geometryType: 'geojson', 47 | outputFormat: 'topojson' 48 | }, function(error, result) { 49 | console.timeEnd('test2') 50 | if (error) { 51 | callback({test: 2, error: error }) 52 | } else { 53 | callback(null) 54 | //console.log(JSON.stringify(result)); 55 | } 56 | }) 57 | }) 58 | }, 59 | function(callback) { 60 | console.time('test3') 61 | /* Select two polygons, returning the geometry as WKT in the field 'wkt' */ 62 | client.query('SELECT name, ST_AsText(geom) AS wkt FROM countries LIMIT 2', function(error, result) { 63 | dbgeo.parse(result.rows, { 64 | geometryColumn: 'wkt', 65 | geometryType: 'wkt', 66 | outputFormat: 'geojson', 67 | precision: 4 68 | }, function(error, result) { 69 | console.timeEnd('test3') 70 | if (error) { 71 | callback({test: 3, error: error }) 72 | } else { 73 | var errors = geojsonhint.hint(result) 74 | if (errors.length) { 75 | console.log(errors) 76 | } 77 | callback(null) 78 | 79 | // console.log(JSON.stringify(result)); 80 | } 81 | }) 82 | }) 83 | }, 84 | function(callback) { 85 | console.time('test4') 86 | /* Select two points, returning a latitude and longitude for each */ 87 | client.query('SELECT name, longitude::float, latitude::float FROM places LIMIT 2', function(error, result) { 88 | dbgeo.parse(result.rows, { 89 | 'geometryColumn': ['longitude', 'latitude'], 90 | 'geometryType': 'll', 91 | 'precision': 6 92 | }, function(error, result) { 93 | console.timeEnd('test4') 94 | if (error) { 95 | callback({test: 4, error: error }) 96 | } else { 97 | var errors = geojsonhint.hint(result) 98 | if (errors.length) { 99 | console.log(errors) 100 | } 101 | callback(null) 102 | 103 | // console.log(JSON.stringify(result)); 104 | } 105 | }) 106 | }) 107 | }, 108 | function(callback) { 109 | console.time('test5') 110 | /* Select two points and WKB geometry */ 111 | client.query('SELECT name, geom FROM places LIMIT 2', function(error, result) { 112 | dbgeo.parse(result.rows, { 113 | precision: 6 114 | }, function(error, result) { 115 | console.timeEnd('test5') 116 | if (error) { 117 | callback({test: 5, error: error }) 118 | } else { 119 | var errors = geojsonhint.hint(result) 120 | if (errors.length) { 121 | callback({test: 5, error: errors}) 122 | } else { 123 | callback(null) 124 | } 125 | // console.log(JSON.stringify(result)); 126 | } 127 | }) 128 | }) 129 | }, 130 | function(callback) { 131 | console.time('test6') 132 | /* Select a null geometry */ 133 | client.query('SELECT 1 AS name, null AS geom', function(error, result) { 134 | dbgeo.parse(result.rows, null, function(error, result) { 135 | console.timeEnd('test6') 136 | if (error) { 137 | callback({test: 6, error: error }) 138 | } else { 139 | var errors = geojsonhint.hint(result) 140 | if (errors.length) { 141 | callback({test: 6, error: errors}) 142 | } else { 143 | callback(null) 144 | } 145 | } 146 | }) 147 | client.end(); 148 | }) 149 | } 150 | ], function(error) { 151 | if (error) { 152 | console.log(error.test, error.error) 153 | process.exit(1) 154 | } 155 | process.exit(0) 156 | }) 157 | --------------------------------------------------------------------------------