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.
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 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!

Comments
No responses yet