Save OpenLayers Feature Data to PostGIS using WFS Transactions

Published

The other day I was using Gaia GPS‘s online tools to create a route and mark points of interest. I thought it would be cool to create these GIS features in an open source application and store the data in my own datastore (PostGIS, naturally).

After some research I found WFS Transactions (WFS-T), which provide the ability to create/update feature data using standard OGC requests. While not much is written about WFS-T, they are supported out of the box in many open source GIS tools like OpenLayers and GeoServer.

A Project was Born

This led to proof of concept application (posted to GitHub) that uses a React/OpenLayers frontend to update GIS feature data stored in a PostGIS database using WFS Transactions (facilitated by GeoServer). It was quite a fun project to work on!

Concept / Goal

The goal was to display a WFS feature on an OpenLayers map and write some data to PostGIS each time the feature was clicked. This was done by including an interation property in the feature data that tracked the number of clicks.

WFS feature displayed in OpenLayers with iteration details at the bottom

Initializing the Backend (GeoServer/PostGIS)

I used the kartoza/docker-geoserver docker recipe to stand up a GeoServer/PostGIS backend to work against. Thanks to Kartoza’s hard work, this was as easy as running docker-compose up in the appropriate directory (more instructions here).

Some configuration was required to create a table and sample record in PostGIS. Once that was complete, a few more steps were required to create a workspace, data store, and layer that published the PostGIS table.

The final step of publishing the layer in GeoServer, and then the fun begins!

The Frontend Application

The frontend application was based on one of my earlier guides discussing how to use React with OpenLayers. Some specific call-outs and lessons learned are shared below, but check out the sample project on GitHub for the complete source code.

Creating the GeoServer WFS Layer in OpenLayers

Defining the WFS Layer and styles was straightforward using the default bbox strategy, used to instruct OpenLayers on how/when to load WFS features 1 2.

import VectorSource from 'ol/source/Vector';
import GeoJSON from 'ol/format/GeoJSON.js';
import {bbox as bboxStrategy} from 'ol/loadingstrategy.js';
import VectorLayer from 'ol/layer/Vector';

const GEOSERVER_BASE_URL = 'http://localhost:8600/geoserver/dev';

// create geoserver generic vector features layer
const featureSource = new VectorSource({
  format: new GeoJSON(),
  url: function (extent) {
    return (
      GEOSERVER_BASE_URL + '/ows?service=WFS&' +
      'version=1.0.0&request=GetFeature&typeName=dev%3Ageneric&maxFeatures=50&' + 
      'outputFormat=application%2Fjson&srsname=EPSG:3857&' +
      'bbox=' +
      extent.join(',') +
      ',EPSG:3857'
    );
  },
  strategy: bboxStrategy,
});

const featureLayer = new VectorLayer({
  source: featureSource,
  style: {
    'stroke-width': 0.75,
    'stroke-color': 'white',
    'fill-color': 'rgba(100,100,100,0.25)',
  },
});

Using React Refs to access OpenLayers objects

When integrating OpenLayers with React, it is important to initialize OpenLayers objects once (e.g. in an onload hook), and use Refs to maintain references to these objects in-between renders.

This also allows the current version of these objects to be accessible in callback functions. Otherwise, a stale version of the object may be provided to the callback (captured at the time of the callback closure).

// react
import React, {  useEffect, useRef } from 'react';
import Map from 'ol/Map'

function MapWrapper(props) {

  // refs are used instead of state to allow integration with 3rd party map onclick callback;
  //  these are assigned at the end of the onload hook
  //  https://stackoverflow.com/a/60643670
  const mapRef = useRef();
  const mapElement = useRef();
  const featuresLayerRef = useRef();

  // other logic removed for brevity

  // react onload hook
  useEffect( () => {

    // create map
    const map = new Map({
      // config removed for brevity
    })

    // save map and featureLary references into React refs
    featuresLayerRef.current = featureLayer;
    mapRef.current = map

  },[])

  return (      
    <div>
      <div ref={mapElement} className="map-container"></div>
    </div>
  ) 

}

export default MapWrapper

In the example above the OpenLayers map, featuresLayer, and even mapElement div are stored as Refs for use in callback functions outside of React.

Executing WFS Transactions from OpenLayers callback functions

The crux of this entire application is sending the WFS Transaction requests to GeoServer with the OpenLayers feature data to write to PostGIS. This is handled in the map onclick callback function.

import WFS from 'ol/format/WFS';
import GML from 'ol/format/GML32';

const GEOSERVER_BASE_URL = 'http://localhost:8600/geoserver/dev';

// map click handler - uses state and refs available in closure
const handleMapClick = async (event) => {

  // get clicked feature from wfs layer
  // TODO: currently only handles a single feature
  const clickedCoord = mapRef.current.getCoordinateFromPixel(event.pixel);
  const clickedFeatures = featuresLayerRef.current.getSource().getFeaturesAtCoordinate(clickedCoord);
  if (!clickedFeatures.length) return; // exit callback if no features clicked
  const feature = clickedFeatures[0];

  // parse feature properties
  const featureData = JSON.parse(feature.getProperties()['data']);

  // iterate prop to test write-back
  if (featureData.iteration) {
    ++featureData.iteration;
  } else featureData.iteration = 1;

  // set property data back to feature
  feature.setProperties({ data: JSON.stringify(featureData) });
  console.log('clicked updated feature data', feature.getProperties())

  // prepare feature for WFS update transaction
  //  https://dbauszus.medium.com/wfs-t-with-openlayers-3-16-6fb6a820ac58
  const wfsFormatter = new WFS();
  const gmlFormatter = new GML({
    featureNS: GEOSERVER_BASE_URL,
    featureType: 'generic',
    srsName: 'EPSG:3857' // srs projection of map view
  });
  var xs = new XMLSerializer();
  const node = wfsFormatter.writeTransaction(null, [feature], null, gmlFormatter);
  var payload = xs.serializeToString(node);

  // execute POST
  await fetch(GEOSERVER_BASE_URL + '/wfs', {
    headers: new Headers({
      'Authorization': 'Basic ' + Buffer.from('admin:myawesomegeoserver').toString('base64'),
      'Content-Type': 'text/xml'
    }),
    method: 'POST',
    body: payload
  });

  // clear wfs layer features to force reload from backend to ensure latest properties
  //  are available
  featuresLayerRef.current.getSource().refresh();

  // display updated feature data on map
  setFeatureData(JSON.stringify(featureData));
}

The code above is executed when the WFS feature is clicked on. This triggers the following logic:

  • Lines 11-14: the clicked OpenLayers feature object is identified
  • Lines 17-25: the feature’s iteration property is increased by 1 and saved back to the feature
  • Lines 30-38: the feature is converted into the proper format for the WFS Transaction (kudos to Dennis Bauszus’ guide for detailing these steps)
  • Lines 41-48: the WFS Transaction request is set to the docker GeoServer instance created earlier in the project
  • Lines 52-55: prompt OpenLayers to reload the WFS data from GeoServer to ensure update properties are present

End-to-end Demonstration

Here is a quick video showing the complete flow, tracing the mouse click on the OpenLayers feature, and the resulting data iteration made back in the PostGIS database table.

Where to go from here

Now that we have an example for how to write GIS data from OpenLayers into PostGIS, we can expand this application to support more complex feature creation and editing. For example, drawing features with OpenLayers. The sky is the limit!

The OpenLayesr Draw Features example

References

  1. OpenLayers BBOX Strategy
  2. ol/loadingstrategy

Subscribe by Email

Enter your email address below to be notified about updates and new posts.


Comments

Loading comments..

No responses yet

Leave a Reply

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