QGIS Server and WFS3
Introduction
There are various map servers available in the Open Source world, each with advantages and disadvantages. Map Server and GeoServer are the most famous, but QGIS provides a map server too, named QGIS Server. One of the main advantages of QGIS Server is the possibility to directly configure the project to serve through QGIS Desktop. Indeed, QGIS Desktop acts like a WYSIWYG tool because the rendering engine used by QGIS Server is exactly the same.
QGIS Server provides some classical OGC services like WMS, WFS, WCS or even WMTS since QGIS 3.4. Furthermore, the certification process has been reached for WMS 1.3.0 since QGIS 3.0 and has also been renewed for the LTR version 3.4.
And from now on, thanks to the work of Alessandro Pasotti in the light of the discussion held on the dedicated QEP, QGIS Server 3.10 is going to provide WFS 3 support. By the way, an experimental driver already exists for GDAL and a prototype has been implemented in GeoServer.
Let’s take a look to the possibilities given by this brand-new protocol.
Quick reminder about WFS 1.X
WFS, or Web Feature Service, is an Open Geospatial Consortium protocol aiming at manipulating vector features (lines, points, polygons, …).
To interact with a map server implementing WFS 1.X, a HTTP request has to be
sent with particular parameters like a NAME
for indicating a
specific layer to work on or a BBOX
to specify an extent. Of
course, there are numerous parameters, and some map servers, like QGIS Server,
implement custom parameters (meaning parameters not described by the OGC
specifications) to tweak settings.
On top of that, WFS 1.X protocol provides mechanisms to explore a project structure
(layers, output format, extents, …) or get feature information. Indeed, it’s
possible thanks to the concept of REQUEST
. The most basic WFS
requests are GetCapabilities
, DescribeFeatureType
and
GetFeature
.
Then, the typical workflow to work with WFS 1.X is to:
- retrieve the structure of the project thanks to the
GetCapabilities
request - ask features’ information for a specific layer with a
GetFeature
request
For instance let’s consider a QGIS Desktop project with, in particular, a layer based on OpenData indicating the position of Dolmens and Menhirs in Brittany.
Then you can retrieve from QGIS Server the structure of the .qgz
project thanks to the next request:
$ curl http://qgisserver? \
SERVICE=WFS \
&VERSION=1.1.0 \
&REQUEST=GetCapabilities \
&PROJECT=/tmp/megaliths.qgz \
> getcapabilities.xml
getcapabilities.xml
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<WFS_Capabilities>
<ows:ServiceIdentification>
<ows:ServiceType>WFS</ows:ServiceType>
<ows:ServiceTypeVersion>1.1.0</ows:ServiceTypeVersion>
<ows:Fees>conditions unknown</ows:Fees>
<ows:AccessConstraints>None</ows:AccessConstraints>
</ows:ServiceIdentification>
<ows:ServiceProvider/>
<ows:OperationsMetadata>
<ows:Operation name="GetCapabilities">
<ows:DCP>
<ows:HTTP>
<ows:Get xlink:href="http://qgisserver?MAP=/tmp/megaliths.qgz"/>
<ows:Post xlink:href="http://qgisserver?MAP=/tmp/megaliths.qgz"/>
</ows:HTTP>
</ows:DCP>
<ows:Parameter name="service">
<ows:Value>WFS</ows:Value>
</ows:Parameter>
<ows:Parameter name="AcceptVersions">
<ows:Value>1.1.0</ows:Value>
<ows:Value>1.0.0</ows:Value>
</ows:Parameter>
<ows:Parameter name="AcceptFormats">
<ows:Value>text/xml</ows:Value>
</ows:Parameter>
</ows:Operation>
<ows:Operation name="DescribeFeatureType">
<ows:DCP>
<ows:HTTP>
<ows:Get xlink:href="http://qgisserver?MAP=/tmp/megaliths.qgz"/>
<ows:Post xlink:href="http://qgisserver?MAP=/tmp/megaliths.qgz"/>
</ows:HTTP>
</ows:DCP>
<ows:Parameter name="outputFormat">
<ows:Value>XMLSCHEMA</ows:Value>
<ows:Value>text/xml; subtype=gml/2.1.2</ows:Value>
<ows:Value>text/xml; subtype=gml/3.1.1</ows:Value>
</ows:Parameter>
</ows:Operation>
<ows:Operation name="GetFeature">
<ows:DCP>
<ows:HTTP>
<ows:Get xlink:href="http://qgisserver?MAP=/tmp/megaliths.qgz"/>
<ows:Post xlink:href="http://qgisserver?MAP=/tmp/megaliths.qgz"/>
</ows:HTTP>
</ows:DCP>
<ows:Parameter name="outputFormat">
<ows:Value>text/xml; subtype=gml/2.1.2</ows:Value>
<ows:Value>text/xml; subtype=gml/3.1.1</ows:Value>
<ows:Value>application/vnd.geo+json</ows:Value>
</ows:Parameter>
<ows:Parameter name="resultType">
<ows:Value>results</ows:Value>
<ows:Value>hits</ows:Value>
</ows:Parameter>
</ows:Operation>
<ows:Operation name="Transaction">
<ows:DCP>
<ows:HTTP>
<ows:Get xlink:href="http://qgisserver?MAP=/tmp/megaliths.qgz"/>
<ows:Post xlink:href="http://qgisserver?MAP=/tmp/megaliths.qgz"/>
</ows:HTTP>
</ows:DCP>
<ows:Parameter name="inputFormat">
<ows:Value>text/xml; subtype=gml/2.1.2</ows:Value>
<ows:Value>text/xml; subtype=gml/3.1.1</ows:Value>
<ows:Value>application/vnd.geo+json</ows:Value>
</ows:Parameter>
</ows:Operation>
</ows:OperationsMetadata>
<FeatureTypeList>
<Operations>
<Operation>Query</Operation>
</Operations>
<FeatureType>
<Name>Megalithe</Name>
<Title>Megalithe</Title>
<DefaultSRS>EPSG:4326</DefaultSRS>
<OtherSRS>EPSG:2154</OtherSRS>
<OtherSRS>EPSG:3857</OtherSRS>
<Operations>
<Operation>Query</Operation>
</Operations>
<OutputFormats>
<Format>text/xml; subtype=gml/3.1.1</Format>
</OutputFormats>
<ows:WGS84BoundingBox dimensions="2">
<ows:LowerCorner>-5.15087 -5.98386</ows:LowerCorner>
<ows:UpperCorner>-1.04853 48.8762</ows:UpperCorner>
</ows:WGS84BoundingBox>
</FeatureType>
</FeatureTypeList>
<ogc:Filter_Capabilities>
<ogc:Spatial_Capabilities>
<ogc:GeometryOperands>
<ogc:GeometryOperand>gml:Point</ogc:GeometryOperand>
<ogc:GeometryOperand>gml:LineString</ogc:GeometryOperand>
<ogc:GeometryOperand>gml:Polygon</ogc:GeometryOperand>
<ogc:GeometryOperand>gml:Envelope</ogc:GeometryOperand>
</ogc:GeometryOperands>
<ogc:SpatialOperators>
<ogc:SpatialOperator name="Equals"/>
<ogc:SpatialOperator name="Disjoint"/>
<ogc:SpatialOperator name="Touches"/>
<ogc:SpatialOperator name="Within"/>
<ogc:SpatialOperator name="Overlaps"/>
<ogc:SpatialOperator name="Crosses"/>
<ogc:SpatialOperator name="Intersects"/>
<ogc:SpatialOperator name="Contains"/>
<ogc:SpatialOperator name="DWithin"/>
<ogc:SpatialOperator name="Beyond"/>
<ogc:SpatialOperator name="BBOX"/>
</ogc:SpatialOperators>
</ogc:Spatial_Capabilities>
<ogc:Scalar_Capabilities>
<ogc:LogicalOperators/>
<ogc:ComparisonOperators>
<ogc:ComparisonOperator>LessThan</ogc:ComparisonOperator>
<ogc:ComparisonOperator>GreaterThan</ogc:ComparisonOperator>
<ogc:ComparisonOperator>LessThanEqualTo</ogc:ComparisonOperator>
<ogc:ComparisonOperator>GreaterThanEqualTo</ogc:ComparisonOperator>
<ogc:ComparisonOperator>EqualTo</ogc:ComparisonOperator>
<ogc:ComparisonOperator>Like</ogc:ComparisonOperator>
<ogc:ComparisonOperator>Between</ogc:ComparisonOperator>
</ogc:ComparisonOperators>
</ogc:Scalar_Capabilities>
<ogc:Id_Capabilities>
<ogc:FID/>
</ogc:Id_Capabilities>
</ogc:Filter_Capabilities>
</WFS_Capabilities>
Upon closer inspection, we may note that only one layer named Megaliths
is available, which is consistent with the server configuration set up in
Project/Properties...
:
Then, we know which layer is available and on which extent. This way, we can
obtain information for specific features with the GetFeature
request:
$ curl http://qgisserver? \
SERVICE=WFS \
&VERSION=1.1.0 \
&REQUEST=GetFeature \
&PROJECT=/tmp/megaliths.qgz \
&TYPENAME=Megaliths \
&MAXFEATURES=3 \
&SRSNAME=EPSG:2154 \
&PROPERTYNAME=denomination,url \
&BBOX=191617.5,6762287.0,209565.0,6772826.7 \
&EXP_FILTER="denomination"='menhir' \
> getfeature.xml
getfeature.xml
<wfs:FeatureCollection>
<gml:boundedBy>
<gml:Envelope srsName="EPSG:4326">
<gml:lowerCorner>-3.79480658 47.76399383</gml:lowerCorner>
<gml:upperCorner>-3.56774131 47.87216407</gml:upperCorner>
</gml:Envelope>
</gml:boundedBy>
<gml:featureMember>
<qgs:Megaliths gml:id="Megaliths.15454">
<qgs:denomination>menhir</qgs:denomination>
<qgs:url>
http://kartenn.region-bretagne.fr/ws/recensement/detail.php?id=divi23_195851
</qgs:url>
</qgs:Megaliths>
</gml:featureMember>
<gml:featureMember>
<qgs:Megaliths gml:id="Megaliths.30909">
<qgs:denomination>menhir</qgs:denomination>
<qgs:url>
http://kartenn.region-bretagne.fr/ws/recensement/detail.php?id=dev17012017_234024
</qgs:url>
</qgs:Megaliths>
</gml:featureMember>
<gml:featureMember>
<qgs:Megaliths gml:id="Megaliths.32185">
<qgs:denomination>menhir</qgs:denomination>
<qgs:url>
http://kartenn.region-bretagne.fr/ws/recensement/detail.php?id=divi23_200251
</qgs:url>
</qgs:Megaliths>
</gml:featureMember>
</wfs:FeatureCollection>
WFS 3, JSON and OpenAPI
Unlike to WFS 1.X where the basic output format is XML, WFS 3.X is based on the
OpenAPI standard and JSON. This way, even if basic mechanisms are identical,
concept of REQUEST
or SERVICE
doesn’t exist anymore.
For example, instead of exploring a project with a GetCapabilities
request, a JSON collection with layers’ capabilities is retrieved with
http://qgisserver/wfs3/collections.json?MAP=/tmp/megaliths.qgz
.
Such a collection is easily parsable in Python with the json
module.
megaliths.py
import requests
import json
http = "http://qgisserver/wfs3/collections.json?MAP=/tmp/megaliths.qgz"
request = requests.get(http)
if request.status_code == 200:
doc = json.loads(request.text)
for layer in doc["collections"]:
if layer["title"] != "Megaliths":
continue
print("Title: ", layer["title"])
print("CRS: ", layer["extent"]["crs"])
print("Extent: ", layer["extent"]["spatial"])
$ python megaliths.py
Title: Megaliths
CRS: http://www.opengis.net/def/crs/OGC/1.3/CRS84
Extent: [[-5.150866731269053, -5.983856309208769, -1.048528989867108, 48.876193000478644]]
Then, to discover Megaliths
layer capabilities, nothing more easy
than retrieving once again a JSON collection with
collections/Megaliths.json?
.
capabilities.py
import requests
import json
http = "http://qgisserver/wfs3/collections/Megaliths.json?MAP=/tmp/megaliths.qgz"
request = requests.get(http)
if request.status_code == 200:
doc = json.loads(request.text)
for link in doc["links"]:
if link["type"] != "application/json":
continue
print(link["href"])
$ python capabilities.py
http://qgisserver/wfs3/collections/Megaliths.json?MAP=/tmp/megaliths.qgz
http://qgisserver/wfs3/collections/Megaliths/items.json?MAP=/tmp/megaliths.qgz
As can be observed, there is a collection/Megaliths/items.json
entry point. Actually, it allows to retrieve features for a dedicated layer,
replacing the so called GetFeature
request. In the same way,
numerous parameters are available. However, there are some notable changes,
especially about the CRS parameter. Indeed, the parameter is now named
bbox-crs
and is defined according to the content of the
crs
property specified within the JSON collection. Moreover, you can
filter by adding field_name=value
directly in the request instead
of using the EXP_FILTER
parameter. Note that a partial matching is
possible thanks to the usual wildcard notation: field_name=v*
.
features.py
import requests
import json
bbox = "191617.5,6762287.0,209565.0,6772826.7"
crs = "http://www.opengis.net/def/crs/EPSG/9.6.2/2154"
filter = "denomination=menhir"
http = ("http://qgisserver/wfs3/collections/Megaliths/items.json?"
"MAP=/tmp/project.qgz"
"&limit=3"
"&{}"
"&bbox={}"
"&bbox-crs={}"
.format(filter, bbox, crs))
request = requests.get(http)
if request.status_code == 200:
doc = json.loads(request.text)
print("Type: ", doc["type"])
print("Number matched: ", doc["numberMatched"])
print("Number returned: ", doc["numberReturned"])
for feature in doc["features"]:
print("Feature URL: ", feature["properties"]["url"])
else:
print("Error")
$ python features.py
Type: FeatureCollection
Number matched: 7
Number returned: 3
Feature URL: http://kartenn.region-bretagne.fr/ws/recensement/detail.php?id=divi23_195851
Feature URL: http://kartenn.region-bretagne.fr/ws/recensement/detail.php?id=dev17012017_234024
Feature URL: http://kartenn.region-bretagne.fr/ws/recensement/detail.php?id=divi23_200251
HTML template
The beautiful thing with OpenAPI specification is the elegant simplicity, meaning that displaying a collection in HTML is pretty easy. This way, QGIS Server 3.10 also provides a default HTML template.
Concretely, this implies that any user may explore the data served by the server
through a simple web page. For instance, the WFS3 landing page for QGIS Server
is available at the next URL:
http://qgisserver/wfs3?MAP=/tmp/megaliths.qgz
.
Then, you can explore the API by clicking on API definition
or
even displaying information on a specific layer thanks to the
Feature collections
link. By the way, we may note that the corresponding HTML
page is accessible through the same request as the one for retrieving layers’
capabilities, but .json
has to be replaced by .html
.
However, if the endpoint is being accessed through a browser, HTML is the
default format even without explicitly adding .html
postfix.
http://qgisserver/wfs3/collections/Megaliths.html?MAP=/tmp/megaliths.qgz
Conclusion
To conclude, not only we can say that Alessandro made an awesome work by adding the WFS3 service from scratch, but QGIS Server has now all elements to implement other services based on the OpenAPI specification. And we are even able to add new OpenAPI services directly with the PyQGIS API, so what’s not to like?
Well, the next step would be the OGC certification, but it’s another story for another time.