Loading

Web GIS on a Shoestring

organizedchaos

This is a live streamed presentation. You will automatically follow the presenter and see the slide they're currently on.

Web GIS on a Shoestring

By Brent Porter

Center for Space Research

University of Texas at Austin

URL 

http://www.slides.com/organizedchaos/deck-8/live/

Why Web GIS on a Shoestring?

I wanted to ask the question...

What do we really need to build a GIS Web application?

And - how cheaply can we build it?

Some Rules

  • Needs to be interactive
  • Needs to incorporate different dynamic datasets
  • Should be 'Nose-to-tail'
    • Table(s) in database
    • Routing Rest Endpoints
    • Server-side Programming
    • Client side Map-UI scripting

What about GIS App Servers???

GIS App Servers are great

I use several different flavors

(ArcGIS Server, GeoServer, MapServer...)

But...

The cost of entry is too high for some

I am talking about opportunity cost here -

  • Require access to physical or virtual hardware
  • Some bit of training or instruction specific to the GIS server

So - No GIS App Server!

So what technology?

I will be using the following open source technologies

  • PostgreSQL
    • gdal
  • NodeJS
  • LeafletJS
  • Javascript

Does it need to be those technologies?

  • No - could use any web serving technology. Or a technology that could serve out web services. So -
    • Java
    • Php
    • Ruby
    • Python
    • etc
  • Could use other technologies also - closed source or for pay, but that defeats the shoestring* idea...

* shoestring - in this context means related to hardware

* shoestring - in this context means related to hardware

Proposed App

  • Will consist of
    • Web Service calls to local data (local database)
    • Web Service calls/interaction with remote data
    • Mapping application to display/tie data together

Architecture Diagram

So - Part 1

Postgresql

  • Mature & stable
  • Spatially enabled
  • Json Friendly
  • & Open Source/Free!

Postgresql - gdal

  • Geospatial Data Abstraction Library
    • www.gdal.org
  • Supports
    • Raster
    • Vector
    • Database
  • Command line interface to access data
    • This example loads json into database using the field names in the json as the schema for the db table
ogr2ogr -f "PostgreSQL" PG:"dbname=geodb 
user=<your user> password=<your password>" 
NamedJSONFile.json -nln 
Name_Of_Table_In_Database -append

Wait... JSON? GeoJSON?

  • www.json.org
  • data exchange format
    • dare I say ubiquitous now?
{"type":"FeatureCollection",
  "features":
[{
  "type":"Feature","properties": {
    "Description": "S0904A0324C_3919_Port_Aransas_Building_Damage",
    "County": "Nueces",
    "Longitude": "-97.049607",
    "Latitude": "27.834263",
    "Source_Image": "http://web.corral.tacc.utexas.edu/CSR/Public/17harvey/TxCAP/20170904/S0904A0324C/S0904A0324C_3919.JPG",
    "Thumbnail": "http://web.corral.tacc.utexas.edu/CSR/Public/17harvey/TxCAP/20170904/S0904A0324C/thumbnails/S0904A0324C_3919_tn.JPG"
  },"geometry":{"type":"Point","coordinates":[-97.049607,27.834263]}},
  {
    "type":"Feature","properties":{"Description": "S0904A0324E_4497_Port_Aransas_Building_Damage",
    "County": "Nueces",
    "Longitude": "-97.05198",
    "Latitude": "27.838837",
    "Source_Image": "http://web.corral.tacc.utexas.edu/CSR/Public/17harvey/TxCAP/20170904/S0904A0324E/S0904A0324E_4497.JPG",
    "Thumbnail": "http://web.corral.tacc.utexas.edu/CSR/Public/17harvey/TxCAP/20170904/S0904A0324E/thumbnails/S0904A0324E_4497_tn.JPG"
  },"geometry":{"type":"Point","coordinates":[-97.05198,27.838837]}},...

Example GeoJSON

Postgresql Examples

WHEN xMinIn,yMinIn,xMaxIn,yMaxIn For a Bounding Box
"SELECT *,ST_XMax(wkb_geometry) as lon_node,ST_YMax(wkb_geometry) as lat_node,
(ST_AsGeoJSON(wkb_geometry)) as locale FROM tx_cap_photo_repository WHERE ST_Intersects"+
"(ST_MakeEnvelope("+xMinIn+","+yMinIn+", "+xMaxIn+","+yMaxIn+", 4326), 
tx_cap_photo_repository.wkb_geometry)";

Returns a collection of points that are within the bounding box (for GPS Photo Collection)

AND
"SELECT row_to_json(fc)"+
        "FROM (SELECT 'FeatureCollection' As type, array_to_json(array_agg(f)) As features "+
        "FROM (SELECT 'Feature' As type, "+
        "    ST_AsGeoJSON(lg.wkb_geometry)::json As geometry,"+
        "    (SELECT row_to_json(t) "+
        "FROM (SELECT id, qpf) t ) As properties "+
        "FROM qpf24hr_day1_oct14 As lg   ) As f )  As fc;";

Returns 
{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon",
"coordinates":[[[-121.4,52.08],[-120.6,51.6],[-120.35,51.09],[-120.32,50.62],[-120.67,49.87],
[-120.65,49.16],[-121.4,52.08]...]]},"properties":{"id":1,"qpf":0.01}}]}

Built-in Functions like ST_XMAX([spatialField]),

ST_AsGeoJSON([spatialField]) & array_to_json([inputArray])

Facilitates integration with web services

  • Web Services ideally should emit text/json back to client api
  • If query to database returns json, then that is less work for web services software
  • Also Postgres/PostGIS represents that a "Geography Aware" component needed to build apps that can do spatial analysis

Onto those web services

  • This talk focuses on NodeJS for web services
    • server side javascript built combining V8 JavaScript engine, an event loop and a low-level I/O api
  • Express npm library (for simplicity)
    • could have used Restify(another npm lib) for a fully featured rest services but wasn't necessary for the scope of this app
    • allows us to map url patterns (with parameters) to function calls back to database
    • can do parameter checks to ensure safe sql queries

Example Endpoints - part 1

To go along with that SQL query from before - where we are constructing JSON 
from the tables in the database, this is the url pattern that will initiate 
that call to get the forecast rain

forecastRainRouter.get('/DataLookup/Forecast/',cors(),query_QPF_1Day,
function(req,res,next){
        res.json(req.qpfForecast);
});


function query_QPF_1Day(req,res,next){
        var sqlQJson = <see earlier code for sql query>
        postgres.client.query(sqlQJson, function (err, results) {
            if (err) {
                console.error(err);
                res.statusCode = 500;
                return res.json({errors: ["Could not retrieve Forecast Rain"]});
            }
            // No results returned mean the object is not found
            if (results.rows[0].Description === null || results.rows.length === 0 
            || results == null) {
                // We are able to set the HTTP status code on the res object
                res.statusCode = 404;
                return res.json({errors: ["Forecast Rain not found"]});
            }
            // console.log(results.rows[0]);
            req.qpfForecast = results.rows[0].row_to_json;
            next();
        });
    }

Example Endpoints - part 2

I am getting the bus stops and pick up times from Capital Metro - 
so in this case, my routes are a proxy or pass through to their data
Note the :route, :stopid and :dir paramterizations
    var capMetroRouter = express.Router();
    //Query CapMetro Route Stops for Given Direction
    capMetroRouter.get('/DataLookup/Capmetro/:route/:dir',cors(),queryCapMetro, function(req,res,next) {
       res.json(req.capRoutes);
    });

    //query CapMetro Upcoming Pickup Times
    capMetroRouter.get('/DataLookup/Capmetro/StopTimes/:route/:stopid',cors(),queryCapMetroUpcomingPickupTimes, function(req,res,next) {
        res.json(req.stopTimes);
    });

    capMetroRouter.param('route', function (req, res, next, route) {
        console.log("Testing on " + route);
        if (isInt(route)) {
            req.route = route;
            next();
        } else {
            res.statusCode = 404;
            return res.json({errors: ["Route not constructed correctly or not recognized - please use numbers only"]});
        }});
    function queryCapMetro(req,res,next){
        fetch('https://www.capmetro.org/planner/s_routetrace.asp?route='+req.params.route+
        '&dir='+req.params.dir+'&stoptrace=B&opt=1&cmp=1')
            .catch(function(err) {
             return res.json({errors: ["Error Will Robinson - Danger Danger!!"]});})
            .then(function(response) {
                return response.json();
            }).then(function(json){
                req.capRoutes = json.stops;
                next();});}

Examples

Examples - Error Results

So - what next

  • We've talked about the database & loading data
  • We demonstrated the rest endpoints which means the server side is built out
  • Now we need to look at what we are doing client side!

Javascript & LeafletJS

  • Javascript will be our client side scripting language
  • More importantly, LeafletJS will be our mapping api (which uses javascript to implement)
    • very easy to work with
    • unobstrusive (doesn't get in the way of how we want to code the rest of the app - I'm looking at you ESRI Arcgis Server Javascript API...)

Some important points

  • The UI that you build is a huge part of the success of any app, if not the single biggest factor
  • You should spend as much time as you are able developing an intuitive, easy to use, mobile friendly UI
  • That being said...

That ain't this talk! :)

  • Instead of discussing the UI, we will be looking at the integration of those endpoints into our mapping framework
  • Purposefully built a minimal interface so we could focus on the interconnections between components to go along with the WebGIS (architecture) on a Shoestring*

Leaflet Data Sources

  • Point, Line and Polygon
  • Raster
  • Graphics objects (square, triangle, circle - these are not quite the same as the polygon above)
  • Plugins for many other data streams
  • And most importantly  -

GeoJSON!

So Let's focus in on Leaflet's GeoJSON features

Important Points

  • First class citizen in the Leaflet ecosystem
  • Easy to set up custom rendering
  • Can create custom info windows/popups
  • Can take both local data sources or remote
    • if constructed properly
L.geoJSON(data, {
    style: function (feature) {
        return {color: feature.properties.color};
    }
}).bindPopup(function (layer) {
    return layer.feature.properties.description;
}).addTo(map);

GeoJSON - continued

  • Need to wire that data value in the geoJSON constructor to our rest endpoints
    • Forecast Rain - see code below
function gatherForecastData(){
            var jqxhrCountyDeclarations = $.ajax({
                crossDomain: true,
                url: '<Your NodeJS Application URL:Port>/api/DataLookup/Forecast/'
            }).err(function(err){
                return {error: err.msg};    
            }).done(function (data) {
               drawForecastRain(data)
            });
        }
function drawForecastRain(qpfDay1Incoming){
            geoJsonQPF_Day1 = L.geoJson(qpfDay1Incoming,
                    {
                        style: function(feature) {
                            switch (feature.properties.qpf) {
                                case 0.010000:return {color: "#79FA00",fillOpacity:0.45,weight: 0.2};
                                case 0.1: return {color: "#00CF00",fillOpacity:0.45,weight: 0.2};
                                case 0.25: return {color: "#008C00",fillOpacity:0.45,weight: 0.2};
                                ...
                                default:
                                    return {color: '#00008A'};
                                    break;
                            }}});
            map.addLayer(geoJsonQPF_Day1);
        }

GeoJSON - continued

  • CapMetro Route Stops -interesting part of code below
function drawCapMetroPoints(incomingPoints,incomingRoute){
            ...
            markers = [];
            $.each(incomingPoints,function(key, value){
                var latLngIn = value.latLng;
                var strAry = latLngIn.split(",");
                var marker = new L.Marker(new L.latLng(strAry[0],strAry[1]), {
                    icon: new L.MakiMarkers.icon({
                        icon: "bus",
                        color: "#cc00ff",
                        size: "m"
                    })
                }).bindPopup(
                        "<p style='background-color: #cccccc'><span style='font-weight:bold'>"+
                        "Stop Id </span>" + value.id +"<br/><span class='stopTimesRetrieval'"+ 
                        "style='font-weight:bold'>Click for Stop Times: <span style='color:red'"+
                        "id='"+value.id+"'>" + value.id +
                        "</span></span><br/><span style='font-weight:bold'>Lat/Lng: </span>" + 
                        strAry[0] + ", " + strAry[1] + "</p>")";

                var tmpValId = '#' + value.id;
                jQuery(document).on('click', tmpValId, function(event){
                    stopTimes(tmpValId.substr(1),incomingRoute);
                });
                markers.push(marker);
            });
            for(var j=0;j<markers.length;j++) {
                map.addLayer(markers[j]);
            }}

Finished Example

Some Housekeeping Items

  • My example is using an ESRI basemap as the underlying structures and cartography on which to lay the bus stops and forecast rain
  • This is through one of those Leaflet plugins called Esri-Leaflet Plugin. It allows us to access arcgis server data/features within leaflet api
  • I didn't HAVE to use the ESRI basemap, I CHOOSE to use it because I like the cartography. You could use any Leaflet-Friendly Tiled Layer in its stead

Wait a second!

Doesn't using those ESRI basemaps cost $$$???

Short Answer:

Nope! ESRI generously provides access to those basemaps free of charge

So how are we doing?

  • We've satisfied our technology stacks
  • We've demoed the finished product

Costs?

  • Database $0
  • NodeJS/Express $0
  • LeafletJS $0
  • Esri Basemaps (through Esri-Leaflet plugin) $0

Total Cost* $0!

* - Another thought on the hardware...

PAAS is a possible solution for 'Shoestringing' your Web GIS

PAAS is Platform as a Service

PAAS Providers

  • OpenShift* by RedHat
  • Azure by Microsoft
  • Heroku* by Heroku
  • Google App Engine by Google

Conclusions

I hope this talk encourages you to look at and experiment with open source libraries and how they can be used  with a modicum of effort to create interesting applications with both commercial and free data sources

All code (including the example web app) is up on Github -

https://github.com/brentporter/ShoeStringWebGIS

Questions?

Made with Slides.com