├── 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 |
--------------------------------------------------------------------------------