├── .gitignore ├── LICENSE ├── README.md ├── controllers ├── core.js └── leonardtown.js ├── helpers ├── geojson.js └── jsonp.js ├── mistake.js ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.js~ 10 | *.*~ 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | node_modules 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Bill Dollins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Node GIS Server 2 | ============ 3 | 4 | Node.js application to provide a GeoJSON-based REST interface to PostGIS data. 5 | 6 | See package.json for dependencies. 7 | 8 | Changelog 9 | --------- 10 | 2014-02-08 - Began implementing MVC as per http://timstermatic.github.io/blog/2013/08/17/a-simple-mvc-framework-with-node-and-express/ 11 | 12 | 2014-02-11 - Added core.js to implement basic feature query. Implemented return of GeoJSON FeatureCollection. 13 | 14 | 2014-02-14 - Added function to perform intersect query using input GeoJSON geometry. Added function to return schema of a specified table. Defaulted all output features to WGS84. 15 | 16 | 2014-02-16 - Added function to return list of layers (geometry, geography, or both). 17 | 18 | 2014-02-17 - Added capability to return either GeometryCollection or FeatureCollection objects, depending upon URL. 19 | 20 | 2014-02-18 - Added leonardtown.js as example of application-specific extension. 21 | 22 | 2014-02-27 - Added CORS and JSONP support 23 | -------------------------------------------------------------------------------- /controllers/core.js: -------------------------------------------------------------------------------- 1 | var pg = require('pg'); 2 | var geojson = require('../helpers/geojson'); 3 | var jsonp = require('../helpers/jsonp'); 4 | 5 | module.exports.controller = function (app) { 6 | 7 | /* enable CORS */ 8 | app.all('*', function (req, res, next) { 9 | res.header("Access-Control-Allow-Origin", "*"); 10 | res.header("Access-Control-Allow-Headers", "X-Requested-With"); 11 | next(); 12 | }); 13 | 14 | /* feature retrieval */ 15 | 16 | /** 17 | * retrieve all features (this could be really slow and is probably not what you really want to do) 18 | */ 19 | app.get('/vector/:schema/:table/:geom', function (req, res, next) { 20 | var client = new pg.Client(app.conString); 21 | var geom = req.params.geom.toLowerCase(); 22 | if ((geom != "features") && (geom != "geometry")) { 23 | res.status(404).send("Resource '" + geom + "' not found"); 24 | return; 25 | } 26 | var schemaname = req.params.schema; 27 | var tablename = req.params.table; 28 | var fullname = schemaname + "." + tablename; 29 | client.connect(); 30 | var crsobj = { 31 | "type" : "name", 32 | "properties" : { 33 | "name" : "urn:ogc:def:crs:EPSG:6.3:4326" 34 | } 35 | }; 36 | var idformat = "'" + req.params.id + "'"; 37 | idformat = idformat.toUpperCase(); 38 | var spatialcol = ""; 39 | var meta = client.query("select * from geometry_columns where f_table_name = '" + tablename + "' and f_table_schema = '" + schemaname + "';"); 40 | meta.on('row', function (row) { 41 | var query; 42 | var coll; 43 | spatialcol = row.f_geometry_column; 44 | if (geom == "features") { 45 | query = client.query("select st_asgeojson(st_transform(" + spatialcol + ",4326)) as geojson, * from " + fullname + ";"); 46 | coll = { 47 | type : "FeatureCollection", 48 | features : [] 49 | }; 50 | } else if (geom == "geometry") { 51 | query = client.query("select st_asgeojson(st_transform(" + spatialcol + ",4326)) as geojson from " + fullname + ";"); 52 | coll = { 53 | type : "GeometryCollection", 54 | geometries : [] 55 | }; 56 | } 57 | query.on('row', function (result) { 58 | if (!result) { 59 | return res.send('No data found'); 60 | } else { 61 | if (geom == "features") { 62 | coll.features.push(geojson.getFeatureResult(result, spatialcol)); 63 | } else if (geom == "geometry") { 64 | var shape = JSON.parse(result.geojson); 65 | //shape.crs = crsobj; 66 | coll.geometries.push(shape); 67 | } 68 | } 69 | }); 70 | 71 | query.on('end', function (err, result) { 72 | res.setHeader('Content-Type', 'application/json'); 73 | res.send(jsonp.getJsonP(req.query.callback, coll)); 74 | }); 75 | 76 | query.on('error', function (error) { 77 | //handle the error 78 | //res.status(500).send(error); 79 | next(); 80 | }); 81 | }); 82 | }); 83 | 84 | /** 85 | * retrieve all features that intersect the input GeoJSON geometry 86 | */ 87 | app.post('/vector/:schema/:table/:geom/intersect', function (req, res, next) { 88 | //console.log(JSON.stringify(req.body)); 89 | var queryshape = JSON.stringify(req.body); 90 | //res.status(501).send('Intersect not implemented'); 91 | var geom = req.params.geom.toLowerCase(); 92 | if ((geom != "features") && (geom != "geometry")) { 93 | res.status(404).send("Resource '" + geom + "' not found"); 94 | return; 95 | } 96 | var client = new pg.Client(app.conString); 97 | var schemaname = req.params.schema; 98 | var tablename = req.params.table; 99 | var fullname = schemaname + "." + tablename; 100 | client.connect(); 101 | var crsobj = { 102 | "type" : "name", 103 | "properties" : { 104 | "name" : "urn:ogc:def:crs:EPSG:6.3:4326" 105 | } 106 | }; 107 | var idformat = "'" + req.params.id + "'"; 108 | idformat = idformat.toUpperCase(); 109 | var spatialcol = ""; 110 | var meta = client.query("select * from geometry_columns where f_table_name = '" + tablename + "' and f_table_schema = '" + schemaname + "';"); 111 | meta.on('row', function (row) { 112 | var query; 113 | var coll; 114 | spatialcol = row.f_geometry_column; 115 | if (geom == "features") { 116 | query = client.query("select st_asgeojson(st_transform(" + spatialcol + ",4326)) as geojson, * from " + fullname + " where ST_INTERSECTS(" + spatialcol + ", ST_SetSRID(ST_GeomFromGeoJSON('" + queryshape + "'),4326));"); 117 | coll = { 118 | type : "FeatureCollection", 119 | features : [] 120 | }; 121 | } else if (geom == "geometry") { 122 | query = client.query("select st_asgeojson(st_transform(" + spatialcol + ",4326)) as geojson from " + fullname + " where ST_INTERSECTS(" + spatialcol + ", ST_SetSRID(ST_GeomFromGeoJSON('" + queryshape + "'),4326));"); 123 | coll = { 124 | type : "GeometryCollection", 125 | geometries : [] 126 | }; 127 | } 128 | 129 | query.on('row', function (result) { 130 | var props = new Object; 131 | if (!result) { 132 | return res.send('No data found'); 133 | } else { 134 | if (geom == "features") { 135 | coll.features.push(geojson.getFeatureResult(result, spatialcol)); 136 | } else if (geom == "geometry") { 137 | var shape = JSON.parse(result.geojson); 138 | //shape.crs = crsobj; 139 | coll.geometries.push(shape); 140 | } 141 | } 142 | }); 143 | 144 | query.on('end', function (result) { 145 | res.setHeader('Content-Type', 'application/json'); 146 | res.send(jsonp.getJsonP(req.query.callback, coll)); 147 | 148 | }); 149 | query.on('error', function (error) { 150 | //handle the error 151 | //res.status(500).send(error); 152 | //next(); 153 | }); 154 | 155 | }); 156 | }); 157 | 158 | /* Schema inspection functions */ 159 | 160 | /* fetch table schema */ 161 | app.get('/vector/layer/:schema/:table/schema', function (req, res, next) { 162 | var client = new pg.Client(app.conString); 163 | var schemaname = req.params.schema; 164 | var tablename = req.params.table; 165 | var fullname = schemaname + "." + tablename; 166 | var sql = "SELECT n.nspname as schemaname,c.relname as table_name,a.attname as column_name,format_type(a.atttypid, a.atttypmod) AS type,col_description(a.attrelid, a.attnum) as comments"; 167 | sql = sql + " FROM pg_class c INNER JOIN pg_namespace n ON c.relnamespace = n.oid LEFT JOIN pg_attribute a ON a.attrelid = c.oid"; 168 | sql = sql + " WHERE a.attnum > 0 and c.relname = '" + tablename + "' and n.nspname = '" + schemaname + "';"; 169 | var retval = { 170 | schema : schemaname, 171 | table : tablename, 172 | columns : [] 173 | }; 174 | client.connect(); 175 | var query = client.query(sql); 176 | query.on('row', function (result) { 177 | var props = new Object; 178 | if (!result) { 179 | return res.send('No data found'); 180 | } else { 181 | retval.columns.push({ 182 | column : result.column_name, 183 | dataType : result.type, 184 | description : result.comments 185 | }); 186 | } 187 | }); 188 | 189 | query.on('end', function (result) { 190 | res.setHeader('Content-Type', 'application/json'); 191 | res.send(jsonp.getJsonP(req.query.callback, retval)); 192 | // 193 | }); 194 | 195 | }); 196 | 197 | /* fetch table schema (not compatible with PostGIS versions prior to 1.5) */ 198 | app.get('/vector/layers/:geotype', function (req, res, next) { 199 | var client = new pg.Client(app.conString); 200 | var sql = "SELECT 'geometry' as geotype, * FROM geometry_columns;"; 201 | if (req.params.geotype.toLowerCase() == "geography") { 202 | sql = "SELECT 'geography' as geotype, * FROM geography_columns;"; 203 | } else if (req.params.geotype.toLowerCase() == "all") { 204 | sql = "SELECT 'geometry' AS geotype, * FROM geometry_columns UNION SELECT 'geography' as geotype, * FROM geography_columns;"; 205 | } 206 | var retval = []; 207 | client.connect(); 208 | var query = client.query(sql); 209 | query.on('row', function (result) { 210 | var props = new Object; 211 | if (!result) { 212 | return res.send('No data found'); 213 | } else { 214 | retval.push(getRow(result, req.params.geotype.toLowerCase())); 215 | } 216 | }); 217 | 218 | query.on('end', function (result) { 219 | res.setHeader('Content-Type', 'application/json'); 220 | res.send(jsonp.getJsonP(req.query.callback, retval)); 221 | }); 222 | 223 | }); 224 | 225 | function getRow(result, geomtype) { 226 | var retval = { 227 | geoType : result.geotype, 228 | database : result.f_table_catalog, 229 | schema : result.f_table_schema, 230 | table : result.f_table_name, 231 | spatialColumn : null, 232 | dimension : result.coord_dimension, 233 | srid : result.srid, 234 | spatialType : result.type 235 | }; 236 | if ((geomtype == "geometry") || (geomtype == "all")) { 237 | retval.spatialColumn = result.f_geometry_column; 238 | } else { 239 | retval.spatialColumn = result.f_geography_column; 240 | } 241 | return retval; 242 | } 243 | } -------------------------------------------------------------------------------- /controllers/leonardtown.js: -------------------------------------------------------------------------------- 1 | var pg = require('pg'); 2 | var geojson = require('../helpers/geojson'); 3 | var jsonp = require('../helpers/jsonp'); 4 | 5 | module.exports.controller = function (app) { 6 | 7 | /* feature retrieval */ 8 | 9 | /** 10 | * retrieve all Leonardtown building of specified property type 11 | */ 12 | app.get('/leonardtown/buildings/:geom', function (req, res, next) { 13 | var client = new pg.Client(app.conString); 14 | var geom = req.params.geom.toLowerCase(); 15 | if ((geom != "features") && (geom != "geometry")) { 16 | res.status(404).send("Resource '" + geom + "' not found"); 17 | return; 18 | } 19 | var tablename = "leonardtown_bldgs"; 20 | var schemaname = "public"; 21 | var fullname = schemaname + "." + tablename; 22 | var spatialcol = ""; 23 | var proptype = req.query.type; 24 | var whereclause = ";"; 25 | if (typeof proptype != "undefined") { 26 | if (proptype.toLowerCase() != "all") { 27 | whereclause = " where structure_ = '" + proptype + "';"; 28 | } 29 | } 30 | var coll; 31 | var sql = ""; 32 | client.connect(function (err) { 33 | //var meta = client.query("select * from geometry_columns where f_table_name = '" + tablename + "' and f_table_schema = '" + schemaname + "';"); 34 | if (err) { 35 | res.status(500).send(err); 36 | //client.end(); 37 | return console.error('could not connect to postgres', err); 38 | } 39 | client.query("select * from geometry_columns where f_table_name = '" + tablename + "' and f_table_schema = '" + schemaname + "';", function (err, result) { 40 | if (err) { 41 | res.status(500).send(err); 42 | client.end(); 43 | return console.error('error running query', err); 44 | } 45 | console.log("meta: " + result.rows.length); 46 | spatialcol = result.rows[0].f_geometry_column; 47 | if (geom == "features") { 48 | sql = "select st_asgeojson(st_transform(" + spatialcol + ",4326)) as geojson, * from " + fullname + whereclause; 49 | coll = { 50 | type : "FeatureCollection", 51 | features : [] 52 | }; 53 | } else if (geom == "geometry") { 54 | sql = "select st_asgeojson(st_transform(" + spatialcol + ",4326)) as geojson from " + fullname + whereclause 55 | coll = { 56 | type : "GeometryCollection", 57 | geometries : [] 58 | }; 59 | } 60 | console.log(spatialcol); 61 | client.query(sql, function (err, result) { 62 | if (err) { 63 | res.status(500).send(err); 64 | client.end(); 65 | return console.error('error running query', err); 66 | } 67 | // console.log(result.rows.length); 68 | for (var i = 0; i < result.rows.length; i++) { 69 | if (geom == "features") { 70 | coll.features.push(geojson.getFeatureResult(result.rows[i], "shape")); 71 | } else if (geom == "geometry") { 72 | var shape = JSON.parse(result.rows[i].geojson); 73 | //shape.crs = crsobj; 74 | coll.geometries.push(shape); 75 | } 76 | } 77 | client.end(); 78 | res.send(jsonp.getJsonP(req.query.callback, coll)); 79 | }); 80 | }); 81 | }); 82 | }); 83 | 84 | } -------------------------------------------------------------------------------- /helpers/geojson.js: -------------------------------------------------------------------------------- 1 | exports.getFeatureResult = function(result, spatialcol) { 2 | var props = new Object; 3 | var crsobj = { 4 | "type" : "name", 5 | "properties" : { 6 | "name" : "urn:ogc:def:crs:EPSG:6.3:4326" 7 | } 8 | }; 9 | //builds feature properties from database columns 10 | for (var k in result) { 11 | if (result.hasOwnProperty(k)) { 12 | var nm = "" + k; 13 | if ((nm != "geojson") && nm != spatialcol) { 14 | props[nm] = result[k]; 15 | } 16 | } 17 | } 18 | 19 | return { 20 | type : "Feature", 21 | crs : crsobj, 22 | geometry : JSON.parse(result.geojson), 23 | properties : props 24 | }; 25 | }; -------------------------------------------------------------------------------- /helpers/jsonp.js: -------------------------------------------------------------------------------- 1 | exports.getJsonP = function (callback, result) { 2 | var retval = ""; 3 | if (typeof callback != "undefined") { 4 | retval = callback.replace("?","") + "({0});"; 5 | retval = String.format(retval, JSON.stringify(result)); 6 | } else { 7 | retval = JSON.stringify(result); 8 | } 9 | return retval; 10 | }; 11 | 12 | String.format = function () { 13 | var s = arguments[0]; 14 | for (var i = 0; i < arguments.length - 1; i++) { 15 | var reg = new RegExp("\\{" + i + "\\}", "gm"); 16 | s = s.replace(reg, arguments[i + 1]); 17 | } 18 | return s; 19 | }; -------------------------------------------------------------------------------- /mistake.js: -------------------------------------------------------------------------------- 1 | var domain=require("domain"); 2 | module.exports = function(func){ 3 | var F = function(){}; 4 | var dom = domain.create(); 5 | F.prototype.catch = function(errHandle){ 6 | var args = arguments; 7 | dom.on("error",function(err){ 8 | return errHandle(err); 9 | }).run(function(){ 10 | func.call(null,args); 11 | }); 12 | return this; 13 | } 14 | return new F(); 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-gis-server", 3 | "description": "Node GIS Server", 4 | "version": "0.0.1", 5 | "private": true, 6 | "contributors": { 7 | "name": "Bill Dollins", 8 | "email": "bill@geomusings.com" 9 | }, 10 | "dependencies": { 11 | "express": "3.x", 12 | "pg": "2.8.3" 13 | }, 14 | "scripts": { 15 | "start": "node server" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | fs = require('fs'); 3 | 4 | 5 | var app = express(); 6 | app.use(express.bodyParser()); 7 | app.use(app.router); 8 | app.conString = "postgres://bdollins:ZAQ!xsw2@localhost:5432/geo2"; 9 | //app.use(error); 10 | 11 | process.on('uncaughtException', function (error) { 12 | console.log(error.stack); 13 | }); 14 | 15 | // dynamically include routes (Controller) 16 | fs.readdirSync('./controllers').forEach(function (file) { 17 | if(file.substr(-3) == '.js') { 18 | route = require('./controllers/' + file); 19 | route.controller(app); 20 | } 21 | }); 22 | 23 | /*function error(err, req, res, next) { 24 | // log it 25 | console.log(err); 26 | //console.error(err.stack); 27 | 28 | // respond with 500 "Internal Server Error". 29 | res.send(500); 30 | }*/ 31 | 32 | app.listen(3000); 33 | console.log('Listening on port 3000...'); 34 | --------------------------------------------------------------------------------