Introducing PostGIS Preview

I’ve been working with PostGIS quite a lot lately, and found myself faced with the problem of being able to easily visualize the results of my queries on a map. CartoDB has spoiled me with a big fat SQL pane and a slippy map that instantly shows you the results of your query, and allows you to iterate quickly and tune your queries to suit your needs. It’s a great workflow, and I found myself wanting for it after standing up PostGIS locally.

I got quite a few responses. Some recommended CLI tools that render maps in the terminal, some proposed programmatically sending the results to geojson.io, some said to use QGIS (but you still have to click a few times to specify the geom column and add the layer, then you would need to delete it before starting the next iteration).

A few minutes later, it occurred to me that this would be super simple to build with leaflet and node.js, so that’s what I did.  I’ll spend the rest of the blog walking through how it works

 

Here’s a basic architecture diagram.  The backend is a simple node.js server that talks to the database and sends the response data to the frontend.  The frontend at its simplest is a leaflet map and a textarea to write SQL into.  Presentation1 2

The Node App

The Node App is a super-simple express.js api with a single endpoint, /sql.  This endpoint expects a PostGIS Query as a URL Parameter “q”.

This query is then passed to pg-promise, which sends it along to the database.

//expose sql endpoint, grab query as URL parameter and send it to the database
app.get('/sql', function(req, res){
  var sql = req.query.q;
  console.log('Executing SQL: ' + sql);

  //query using pg-promise
  db.any(sql)
    .then(function (data) { //use dbgeo to convert WKB from PostGIS into topojson
        return dbGeoParse(data);
    })
    .then(function (data) {
        console.log("DATA:", data);
        res.send(data);
    })
    .catch(function (err) { //send the error message if the query didn't work
        var msg = err.message || err;
        console.log("ERROR:", msg);
        res.send({
            error: msg
        });
    });
});

When the response from the database comes back, it is passed into dbgeo which transforms it to topoJson. The Topojson is then send as the response to the browser. (Topojson is used instead of geoJson to make the size of the response smaller as we have to get everything into the browser to render it on a map)

function dbGeoParse(data) {
    return new Promise(function (resolve, reject) {
        dbgeo.parse({
            data: data,
            outputFormat: 'topojson',
            geometryColumn: 'geom',
            geometryType: 'wkb'
        }, function (err, result) {
            if (err) {
                reject(err);
            } else {
                resolve(result);
            }
        });
    });
}

That’s about it, the whole backend is 60 lines of code plus dependencies!

The Frontend

On the frontend, we have a single HTML page with a leaflet map and a textarea to write your SQL in.

When you click the submit button after entering a query, it is sent to the backend as a URL param using Jquery’s getJSON() method.

      //listen for submit of new query
      $('form').submit(function(e) {
        e.preventDefault();

        clearTable();

        var sql = $('#sqlPane').val();
        
        //clear the map
        if( map.hasLayer(layer)) {
          layer.clearLayers();
        }
      
        //pass the query to the sql api endpoint
        $.getJSON('/sql?q=' + sql, function(data) {
          console.log(data)
          if(!data.error) {
            //convert topojson coming over the wire to geojson using mapbox omnivore
            var features = omnivore.topojson.parse(data); //should this return a featureCollection?  Right now it's just an array of features.
            addLayer( features ); //draw the map layer
            buildTable( features ); //build the table
            
          } else {
            //write the error in the sidebar
            $('#notifications').text(data.error)
          }
        })
      })

The topojson response is processed using omnivore, a great little JS library by mapbox that can convert various geospatial data formats. The output is geoJSON geometries that can be rendered in Leaflet using L.geoJson().

      function addLayer( features ) {
        //create an L.geoJson layer, add it to the map
          layer = L.geoJson(features, {
            style: {
                color: '#fff', // border color
                fillColor: 'steelblue',
                weight: 1,
                opacity: 1,
                fillOpacity: 0.7
            },

            pointToLayer: function ( feature, latlng ) {
              return L.circleMarker(latlng, {
                radius: 4,
                fillColor: "#ff7800",
                color: "#000",
                weight: 1,
                opacity: 1,
                fillOpacity: 0.8
              });
            }
          }).addTo(map)

          map.fitBounds(layer.getBounds());
          $('#notifications').empty();
      }

Here it is in action:
7e8088cc-0d52-11e6-9c0e-b56f3b2af954

No frills, just sweet, sweet geometries on a slippy map. I’ve since added a data view using bootstrap datatables. Some other enhancements I’d like to add:

– Add Codemirror instead of a text area for pretty SQL formatting.
– Use localstorage or something to store the last n queries so you can recall them later.
– A simple popup for each geom to show all of the associated attributes

Pull requests are welcome!  Here’s the code on github. Please give it a try and tweet at @chris_whong to let me know what you think.

Leave a Reply

Your email address will not be published. Required fields are marked *