├── settings.js.example ├── .gitignore ├── mistake.js ├── package.json ├── helpers ├── jsonp.js └── geojson.js ├── index.html ├── server.js ├── LICENSE ├── README.md ├── startupscript.sh ├── neighbors.html ├── countryclick.html ├── controllers └── core.js ├── az.html └── azclick.html /settings.js.example: -------------------------------------------------------------------------------- 1 | var data = { 2 | database : "postgres://postgres:hello12345hello@localhost:5432/sjmdatabase" 3 | } 4 | 5 | module.exports = data; 6 | -------------------------------------------------------------------------------- /.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 | settings.js 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 | "name": "Mano Marks", 11 | "url": "https://google.com/+ManoMarks" 12 | } 13 | ], 14 | "dependencies": { 15 | "express": "3.x", 16 | "pg": "2.8.3" 17 | }, 18 | "scripts": { 19 | "start": "node server" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 |

Using PostGIS, Node.js, and Google Maps on top of Compute Engine

16 | 17 |
PostGIS intersects query (countries of the world) 18 |
19 |
20 |
AZ lakes, cities, wilderness load from postgres 21 |
22 |
23 |
Click on AZ to see lakes 24 | 25 | 26 | -------------------------------------------------------------------------------- /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 | 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 | console.log("Loaded Controller: ", file); 19 | route = require('./controllers/' + file); 20 | route.controller(app); 21 | } 22 | 23 | }); 24 | 25 | function error(err, req, res, next) { 26 | // log it 27 | console.log(err); 28 | console.error(err.stack); 29 | 30 | // respond with 500 "Internal Server Error". 31 | res.send(500); 32 | } 33 | app.use(express.static(__dirname + '/')); 34 | 35 | app.listen(3000); 36 | console.log('Listening on port 3000...'); 37 | -------------------------------------------------------------------------------- /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 | In support of AGIC Symposium 2014 presentation: https://github.com/tooshel/agic2014 5 | 6 | Node.js application to provide a GeoJSON-based REST interface to PostGIS data. 7 | 8 | This code is based on code originally released by Bill Dollins, as described here: http://blog.geomusings.com/2014/02/18/a-little-deeper-with-node-and-postgis 9 | 10 | This code is designed to run on Google Compute Engine using a backports wheezy Debian instance. startupscript.sh installs all the dependencies, creates the database, downloads country border data from http://naturalearthdata.com and loads it into the database. This can take several minutes. You can monitor progress in /var/log/startupscript.log. 11 | 12 | To run the server, run node server from the /src/node-gis-server directory. It will then be available at your IP address. countryclick.html displays a map. When you click on a country it loads the boundary data and displays it on the map using. 13 | 14 | before you run the startup script, change all instances of "manotest" to your own database name, and all instances of "mmarks" to your own username on Compute Engine. 15 | 16 | 17 | See package.json for dependencies. 18 | 19 | -------------------------------------------------------------------------------- /startupscript.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # before you start, change all instances of "mmarks" to your username and all instances of "sjmdatabase" to your database name 3 | cd /home/sheldon 4 | sudo apt-get update 5 | sudo apt-get -y install build-essential 6 | sudo apt-get -y install build-essential postgresql-9.1 postgresql-server-dev-9.1 libxml2-dev libproj-dev libjson0-dev libgeos-dev xsltproc docbook-xsl docbook-mathml 7 | sudo apt-get -y install libproj-dev 8 | sudo apt-get -y install libgdal-dev 9 | wget http://download.osgeo.org/postgis/source/postgis-2.0.4.tar.gz 10 | tar xfz postgis-2.0.4.tar.gz 11 | cd postgis-2.0.4 12 | ./configure 13 | make 14 | sudo make install 15 | sudo ldconfig 16 | sudo make comments-install 17 | sudo ln -sf /usr/share/postgresql-common/pg_wrapper /usr/local/bin/shp2pgsql 18 | sudo ln -sf /usr/share/postgresql-common/pg_wrapper /usr/local/bin/pgsql2shp 19 | sudo ln -sf /usr/share/postgresql-common/pg_wrapper /usr/local/bin/raster2pgsql 20 | sudo apt-get -y install gdal-bin 21 | sudo apt-get -y install python-gdal 22 | sudo apt-get -y install git 23 | cd /home/sheldon 24 | wget https://s3.amazonaws.com/json-c_releases/releases/json-c-0.11-nodoc.tar.gz 25 | tar xfz json-c-0.11-nodoc.tar.gz 26 | cd json-c-0.11 27 | ./configure 28 | make 29 | make check 30 | sudo make install 31 | sudo apt-get -y install python g++ make checkinstall 32 | src=$(mktemp -d) && cd $src 33 | wget -N http://nodejs.org/dist/node-latest.tar.gz 34 | tar xzvf node-latest.tar.gz && cd node-v* 35 | ./configure 36 | fakeroot checkinstall -y --install=no --pkgversion $(echo $(pwd) | sed -n -re's/.+node-v(.+)$/\1/p') make -j$(($(nproc)+1)) install 37 | sudo dpkg -i node_* 38 | sudo apt-get -y install unzip 39 | sudo apt-get -y install libpq-dev 40 | sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 3000 41 | cd /home/sheldon 42 | sudo -u postgres createuser root -s 43 | createdb sjmdatabase 44 | psql sjmdatabase -c "CREATE EXTENSION POSTGIS;" 45 | echo "tried to create extension" 46 | psql sjmdatabase -c "ALTER USER postgres WITH PASSWORD 'postgres';" 47 | echo "tried to alter password" 48 | #change sjmdatabase to your database name 49 | cd /home/sheldon 50 | mkdir data 51 | cd data 52 | wget http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/110m_cultural.zip 53 | unzip 110m_cultural.zip 54 | cd 110m_cultural 55 | #change sjmdatabase to your database name 56 | ogr2ogr -t_srs EPSG:4326 -f PostgreSQL -overwrite -lco GEOMETRY_NAME=wkb_geometry -lco ENCODING="Windows 1252" -clipsrc -180 -85.05112878 180 85.05112878 -nlt MULTIPOLYGON -nln countries PG:"dbname='sjmdatabase' " ne_110m_admin_0_countries.shp 57 | 58 | -------------------------------------------------------------------------------- /neighbors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PostGIS Country Boundaries 5 | 6 | 7 | 23 | 24 | 25 | 107 | 108 | 109 |
110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /countryclick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PostGIS Country Boundaries 5 | 6 | 7 | 23 | 33 | 34 | 35 | 110 | 111 | 112 |
113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /controllers/core.js: -------------------------------------------------------------------------------- 1 | var pg = require('pg'); 2 | var geojson = require('../helpers/geojson'); 3 | var jsonp = require('../helpers/jsonp'); 4 | var settings = require('../settings'); 5 | 6 | module.exports.controller = function(app) { 7 | 8 | /* enable CORS */ 9 | app.all('*', function(req, res, next) { 10 | res.header('Access-Control-Allow-Origin', '*'); 11 | res.header('Access-Control-Allow-Headers', 'X-Requested-With'); 12 | next(); 13 | }); 14 | 15 | app.get('/vector/:schema/:table/:geom/intersect', function(req, res, next) { 16 | var queryshape = ' {"type": "Point", "coordinates": [' + req.query['lng'] + ',' + req.query['lat'] + '] }'; 17 | var geom = req.params.geom.toLowerCase(); 18 | if ((geom != 'features') && (geom != 'geometry') && (geom != 'all')) { 19 | res.status(404).send("Resource '" + geom + "' not found"); 20 | return; 21 | } 22 | var schemaname = req.params.schema; 23 | var tablename = req.params.table; 24 | var fullname = schemaname + '.' + tablename; 25 | pg.connect(settings.database, function(err, client, done) { 26 | // var spatialcol = 'wkb_geometry'; 27 | var spatialcol = 'geom'; 28 | var sql; 29 | var coll; 30 | if (geom == 'features') { 31 | sql = 'select st_asgeojson(st_transform(' + spatialcol + ',4326)) as geojson, * from ' + tablename + ' where ST_INTERSECTS(' + spatialcol + ", ST_SetSRID(ST_GeomFromGeoJSON('" + queryshape + "'),4326));"; 32 | coll = { 33 | type: 'FeatureCollection', 34 | features: [] 35 | }; 36 | query = client.query(sql); 37 | } 38 | 39 | if (geom == 'all') { 40 | sql = 'select st_asgeojson(st_transform(' + spatialcol + ',4326)) as geojson, * from ' + tablename; 41 | coll = { 42 | type: 'FeatureCollection', 43 | features: [] 44 | }; 45 | query = client.query(sql); 46 | } 47 | 48 | query.on('row', function(result) { 49 | var props = new Object; 50 | if (!result) { 51 | return res.send('No data found'); 52 | } 53 | else { 54 | if (geom == 'features' || geom == 'all') { 55 | coll.features.push(geojson.getFeatureResult(result, spatialcol)); 56 | } else if (geom == 'geometry') { 57 | var shape = JSON.parse(result.geojson); 58 | coll.geometries.push(shape); 59 | } 60 | } 61 | }); 62 | 63 | query.on('end', function(err, result) { 64 | res.setHeader('Content-Type', 'application/json'); 65 | res.send(jsonp.getJsonP(req.query.callback, coll)); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | app.get('/neighbor/:schema/:table/:geom/intersect', function(req, res, next) { 71 | var queryshape = "'SRID=4326;POINT(" + req.query['lng'] +' ' + req.query['lat'] + ")'"; 72 | var geom = req.params.geom.toLowerCase(); 73 | if ((geom != 'features') && (geom != 'geometry')) { 74 | res.status(404).send("Resource '" + geom + "' not found"); 75 | return; 76 | } 77 | var schemaname = req.params.schema; 78 | var tablename = req.params.table; 79 | var fullname = schemaname + '.' + tablename; 80 | pg.connect(settings.database, function(err, client, done) { 81 | var spatialcol = 'wkb_geometry'; 82 | var sql; 83 | var coll; 84 | if (geom == 'features') { 85 | sql = 'SELECT ST_AsGeoJson(ST_Transform(b.' + spatialcol + ',4326)) as geojson, * from ' + tablename + ' as a, ' + tablename + ' as b where st_distance(a.' + spatialcol + ',b.' + spatialcol + ') < .00005 and ST_INTERSECTS(a.' + spatialcol + ', ST_GeographyFromText(' + queryshape + '));' 86 | //sql = 'SELECT ST_AsGeoJson(ST_Transform(b.' + spatialcol + ',4326)) as geojson, * from ' + tablename + ' as a, ' + tablename + ' as b where st_touches(a.' + spatialcol + ',b.' + spatialcol + ') and ST_INTERSECTS(a.' + spatialcol + ', ST_GeographyFromText(' + queryshape + '));' 87 | coll = { 88 | type: 'FeatureCollection', 89 | features: [] 90 | }; 91 | query = client.query(sql); 92 | } 93 | 94 | query.on('row', function(result) { 95 | var props = new Object; 96 | if (!result) { 97 | return res.send('No data found'); 98 | } 99 | else { 100 | if (geom == 'features') { 101 | coll.features.push(geojson.getFeatureResult(result, spatialcol)); 102 | } else if (geom == 'geometry') { 103 | var shape = JSON.parse(result.geojson); 104 | coll.geometries.push(shape); 105 | } 106 | } 107 | }); 108 | 109 | query.on('end', function(err, result) { 110 | res.setHeader('Content-Type', 'application/json'); 111 | res.send(jsonp.getJsonP(req.query.callback, coll)); 112 | done(); 113 | }); 114 | }); 115 | }); 116 | 117 | }; -------------------------------------------------------------------------------- /az.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PostGIS AZ 5 | 6 | 7 | 23 | 33 | 34 | 35 | 163 | 164 | 165 |
166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /azclick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PostGIS AZ 5 | 6 | 7 | 23 | 33 | 34 | 35 | 165 | 166 | 167 |
168 | 169 | 170 | 171 | 172 | --------------------------------------------------------------------------------