Skip to content

Web Feature Services

Overview

In this exercise we'll create a Mapfile that can be used to serve data as a WFS. We'll be using the "Populated Places" from Natural Earth as the data source.

Configuring a MAP for WFS

Similar to other OGC services, setting up a Mapfile to serve a WFS uses keywords in METADATA blocks.

By default WFS output is XML. We can however configure it to output other formats such as GeoJSON by adding an OUTPUTFORMAT block to the Mapfile:

OUTPUTFORMAT
    NAME "geojson"
    DRIVER "OGR/GEOJSON"
    MIMETYPE "application/json; subtype=geojson; charset=utf-8"
    FORMATOPTION "FORM=SIMPLE"
    FORMATOPTION "STORAGE=memory"
    FORMATOPTION "LCO:NATIVE_MEDIA_TYPE=application/vnd.geo+json"
    FORMATOPTION "USE_FEATUREID=true" # ensure GeoJSON output has an id property
END

We then need to add this format to the list of formats returned by the service:

WEB
  METADATA
    ...
    "wfs_getfeature_formatlist" "geojson" # we could also return more complex types such as shapezip
  END
END

We also need to make sure that any projection requested by a client application is allowed:

WEB
  METADATA
    ...
    "wfs_srs" "EPSG:4326 EPSG:3857"
    # we can also use ows_ to set these properties for all OWS services
    # such as WMS, WFS, and WCS
    # "ows_srs" "EPSG:4326 EPSG:3857"
  END
END

See the WFS Server documentation for more details.

Configuring a LAYER for WFS

At the LAYER level there are some additional settings that need to be configured.

Tip

It is good practice to set an EXTENT on the LAYER. If not set then MapServer tries to calculate this dynamically so it can return the extent in requests such as GetCapabilities. This can dramatically slow down the performance of the layer.

We set a unique field name in the METADATA using gml_featureid. Without this not all features may be returned.

We also need to configure which feature properties are returned by the service. We can provide a list of field names, or we can use the all keyword to return all properties.

We can also manually define the field type for each property, or we can let MapServer calculate these from the source dataset using "gml_types" "auto".

LAYER
  ...
  METADATA
      "gml_featureid" "ne_id"
      "gml_include_items" "all"
      "gml_types" "auto"
  END

The METADATA blocks are very flexible, and allow different titles to be applied to the layer for different services, for example:

LAYER
  ...
  METADATA
      "wfs_title" "World Cities"
      "wms_title" "Cities of the World"
      ...
  END

Other Mapfile Notes

The Mapfile contains a LAYER FILTER to limit the features in the layer.

FILTER ([pop_max] > 1000000) # only return places with a population > 1 million

Requesting a WFS in OpenLayers

In OpenLayers we create a VectorLayer with a VectorSource. The URL for the layer specifies GeoJSON as the format to use: &outputFormat=geojson.

The code used for this example is based on the WFS example. Every time the OpenLayers map is moved a request is made to return features.

const vectorSource = new VectorSource({
    format: new GeoJSON(),
    url: function (extent) {
        const url = mapserverUrl + mapfilesPath + 'wfs.map&service=WFS&' +
            'version=2.0.0&request=GetFeature&typename=places&' +
            'outputFormat=geojson&crsName=EPSG:3857&' +
            'bbox=' +
            extent.join(',') +
            ',EPSG:3857';
        return url;
    },
    strategy: bboxStrategy,
});

As a WFS returns raw features we need to apply styling in the client. In this example we create a function that returns a circle style:

function createStyle(feature) {
    return new Style({
        image: new CircleStyle({
            radius: 5 + feature.get('rank_min'),
            fill: new Fill({
                color: [255, 153, 0, 0.8],
            }),
        }),
        ...
    });
}
...
  new VectorLayer({
      style: createStyle,
      source: vectorSource
  }),

Testing on the Command Line

We can test the Mapfile and WFS responses on the command line as follows:

docker exec -it mapserver /bin/bash
mapserv -nh "QUERY_STRING=map=/etc/mapserver/wfs.map&service=WFS&version=2.0.0&request=GetFeature&typeName=places&outputFormat=geojson&crsName=EPSG:3857&bbox=-59223902.72157662,-3903081.7252075593,-14974405.131250374,19995821.45447336,EPSG:3857"

Code

wfs.js
import '../css/style.css';
import GeoJSON from 'ol/format/GeoJSON.js';
import Map from 'ol/Map.js';
import OSM from 'ol/source/OSM.js';
import VectorSource from 'ol/source/Vector.js';
import View from 'ol/View.js';
import {
    Circle as CircleStyle,
    Fill,
    Stroke,
    Style,
    Text,
} from 'ol/style.js';
import { Tile as TileLayer, Vector as VectorLayer } from 'ol/layer.js';
import { bbox as bboxStrategy } from 'ol/loadingstrategy.js';

// based on the example at https://openlayers.org/en/latest/examples/vector-wfs.html

const mapserverUrl = import.meta.env.VITE_MAPSERVER_BASE_URL;
const mapfilesPath = import.meta.env.VITE_MAPFILES_PATH;

const vectorSource = new VectorSource({
    format: new GeoJSON(),
    url: function (extent) {
        const url = mapserverUrl + mapfilesPath + 'wfs.map&service=WFS&' +
            'version=2.0.0&request=GetFeature&typename=places&' +
            'outputFormat=geojson&crsName=EPSG:3857&' +
            'bbox=' +
            extent.join(',') +
            ',EPSG:3857';
        console.log(url);
        return url;
    },
    strategy: bboxStrategy,
});

const textFill = new Fill({
    color: '#fff',
});
const textStroke = new Stroke({
    color: 'rgba(0, 0, 0, 0.6)',
    width: 3,
});

function createStyle(feature) {
    return new Style({
        image: new CircleStyle({
            radius: 5 + feature.get('rank_min'),
            fill: new Fill({
                color: [255, 153, 0, 0.8],
            }),
        }),
        text: new Text({
            text: feature.get('name'),
            fill: textFill,
            stroke: textStroke,
        }),
    });
}

const layers = [
    new TileLayer({
        source: new OSM(),
        className: 'bw',
    }),
    new VectorLayer({
        style: createStyle,
        source: vectorSource
    }),
];

const map = new Map({
    layers: layers,
    target: 'map',
    view: new View({
        center: [2975862.75916499, 8046369.8646329],
        zoom: 5,
    }),
});
wfs.map
wfs.map
MAP
    NAME "WFS"
    EXTENT -180 -90 180 90
    SIZE 400 400 #
    PROJECTION
        "init=epsg:4326"
    END

    OUTPUTFORMAT
        NAME "geojson"
        DRIVER "OGR/GEOJSON"
        MIMETYPE "application/json; subtype=geojson; charset=utf-8"
        FORMATOPTION "FORM=SIMPLE"
        FORMATOPTION "STORAGE=memory"
        FORMATOPTION "LCO:NATIVE_MEDIA_TYPE=application/vnd.geo+json"
        FORMATOPTION "USE_FEATUREID=true" # ensure GeoJSON output has an id property
    END

    WEB
        METADATA
            "ows_enable_request" "*" # this enables all OGC requests
            "wfs_getfeature_formatlist" "geojson"
            "wfs_srs" "EPSG:4326 EPSG:3857"
            "ows_onlineresource" "http://localhost:5000/"
        END
    END
    LAYER
        NAME "places"
        TYPE POINT
        PROJECTION
            "init=epsg:4326"
        END
        EXTENT -180.0 -90.0 180.0 90
        STATUS OFF
        METADATA
            "gml_featureid" "ne_id"
            "gml_include_items" "all"
            "gml_types" "auto"
        END
        FILTER ([pop_max] > 1000000) # only return places with a population > 1 million
        CONNECTIONTYPE OGR
        CONNECTION "data/naturalearth"
        DATA "ne_50m_populated_places_simple"
        CLASS
            STYLE
                COLOR 60 179 113
                OUTLINECOLOR 255 255 255
                OUTLINEWIDTH 0.1
            END
        END
    END
END

Exercises

  1. Change the MAP and LAYER WFS metadata, and view the GetCapabilities document.
  2. Try limiting the gml_include_items to a single attribute name.
  3. Try adding a new shapezip OUTPUTFORMAT, and testing the response on the command line. You can redirect it to a file using > test.zip. Remember to add the format to wfs_getfeature_formatlist in the Mapfile, and to outputFormat in the request string.

    OUTPUTFORMAT
        NAME "shapezip"
        DRIVER "OGR/ESRI Shapefile"
        FORMATOPTION "STORAGE=filesystem"
        FORMATOPTION "FORM=zip"
        FORMATOPTION "LCO:SPATIAL_INDEX=YES"
        FORMATOPTION "LCO:ADJUST_TYPE=YES"
        FORMATOPTION "LCO:RESIZE=YES"
    END