Clicky




QGIS Server and WFS3


dateSeptember 18, 2019

tagsQGIS Server, OGC, WFS3

avatarPaul Blottiere and Alessandro Pasotti


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.


OGC Badge

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:

  1. retrieve the structure of the project thanks to the GetCapabilities request
  2. 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.


OGC Badge

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


OGC Badge

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


Pie Chart


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.


OGC Badge

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


OGC Badge


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.