A Guide for Developers Traveling Through OpenLayers - 15. Using WFS GetFeature to Display Objects on the Map
A Guide for Developers Traveling Through OpenLayers - 15. Using WFS GetFeature to Display Objects on the Map
Up to now, everything covered has been purely a feature of OpenLayers itself, but starting from this chapter, we'll gradually cover integration with GeoServer.
The first feature we'll cover is WFS. In GeoServer, WFS returns the information of the specified elements in GeoJSON form. This information can be used appropriately to display it on the map.
Through this feature, you can display data you directly manage or process on the map.
To display WFS, a total of 5 objects are needed. These are the VectorSource that holds the GeoJSON result of WFS, the VectorLayer that renders the map using the VectorSource, and the remaining View and Map objects. Additionally, you can describe the representation method via the Style object.
We'll explain how to implement these 5 elements in order, and ultimately build a map that leverages WFS.
Since we've already built the data through GeoServer, GeoServer can handle WFS requests for that layer. Let's construct the WFS call URL.
Among WFS operations, we use GetFeature, which provides attribute information. The request method for GetFeature is as follows.
TXT
GET https://example.com/geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typename=test:building&srsName=EPSG:3857&outputFormat=application/json&bbox=14168809.936013725,4366042.924151548,14170735.193663657,4367768.7289308,EPSG:3857
| Parameter | Example | Require | Description |
|---|---|---|---|
| service | WFS (fixed) | Y | Service name |
| version | 2.0.0 (default), 1.1.0, 1.0.0 | Y | Version |
| request | GetFeature (fixed) | Y | Request name |
| typename | repo_name:layer_name | Y | Layer name (multiple separated by commas) |
| srsName | EPSG:4326 | Base coordinate system (if left empty, the layer's default coordinate system is used) | |
| outputFormat | application/vnd.ogc.se_xml (default) | Response format | |
| exceptions | application/vnd.ogc.se_xml (default) | Exception response format | |
| propertyName | All columns | Column names to include in the response (multiple separated by commas) | |
| bbox | ,EPSG:0000 | Range to limit to | |
| featureID | {id} | Feature ID |
Construct the URL according to the information of the layer you've built.
OpenLayers' VectorSource object can directly parse the GeoJSON it receives. Thanks to this, if the GeoJSON is well-formed, it can easily be applied without any additional configuration or mapping process.
Create a VectorSource based on the WFS URL created above.
TYPESCRIPT
import { Vector as VectorSource } from 'ol/source'; import { GeoJSON } from 'ol/format'; import { bbox } from 'ol/loadingstrategy'; const wfs = new VectorSource({ format: new GeoJSON(), url: (extent) => `https://example.com/geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typename=test:building&srsName=EPSG%3A3857&outputFormat=application%2Fjson&exceptions=application%2Fjson&bbox=${extent[0]}%2C${extent[1]}%2C${extent[2]}%2C${extent[3]}%2CEPSG%3A3857`, strategy: bbox });
You can check the full information about VectorSource in the official documentation.
It was originally Vector, but the name has been changed to VectorSource for a clearer expression here.
VectorSource allows you to configure desired options in JSON form, and the above configuration inputs the most basic setting values.
| Name | Type | Default | Description |
|---|---|---|---|
| attributions | ol/source/Source-AttributionLike | undefined | Attribution text (bottom-right of map) | |
| features | Array<ol/Feature-Feature> | ol/Collection-Collection<ol/Feature-Feature> | undefined | Feature array | |
| format | ol/format/Feature-FeatureFormat | undefined | The format used by the URL data loader to recognize data. Required if url is set | |
| loader | ol/featureloader-FeatureLoader | undefined | Loader method. If not specified, the default loader is used. The features load end and features load error events only occur when success and failure callbacks are used | |
| overlaps | boolean | true | How overlapping geometry is handled. If false, the renderer optimizes the geometry's boundary and fill work |
| strategy | ol/source/Vector-LoadingStrategy | undefined | The data rendering strategy. By default, it uses ol/loadingstrategy.all, which loads all Features at once | |
| url | string | ol/featureloader-FeatureUrlFunction | undefined | Data URL | |
| useSpatialIndex | boolean | true | Whether to use a spatial index. If features change frequently or there are few of them, setting this to false improves speed. |
| wrapX | boolean | true | Whether to wrap horizontally |
If you access the URL assigned to url, it returns GeoJSON that meets the conditions. Since VectorSource's format is GeoJSON, the data can be parsed.
Since loading is based on the currently viewed map area, it's convenient for calling data corresponding to the map's area.
For other options and methods you can use, check ol/source/Vector-VectorSource.
TYPESCRIPT
const url = `https://example.com/geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typename=test:building&srsName=EPSG%3A3857&outputFormat=application%2Fjson&exceptions=application%2Fjson&bbox=${extent[0]}%2C${extent[1]}%2C${extent[2]}%2C${extent[3]}%2CEPSG%3A3857`
If the URL is constructed like this, it's easy to check the result of the URL construction, but each piece of data doesn't come into view at a glance. This form makes it easy to miss small mistakes like typos, and constructing the URL manually is also tiring.
To solve this, I created a method that receives data in JSON form and converts it into a URL Query form.
TYPESCRIPT
/** * URL builder method * * @param {string} host: host * @param {{ [ key: string ]: string | number | boolean | undefined }} query: query parameters * * @returns {string} URL */ export function urlBuilder(host: string, query: { [ key: string ]: string | number | boolean | undefined }) { const param = Object.entries(query).map(([ key, value ]) => value ? `${key}=${encodeURIComponent(value)}` : '').join('&'); return `${host}?${param}`; } // https://example.com/geoserver/wfs?name=steve&age=18&actived=true urlBuilder('https://example.com/geoserver/wfs', { name: 'steve', age: 18, actived: true });
- host: The target URL to use with the query
- The address part before the ? in https://example.com/test?query=1
- query: A JSON object
Using the above method, the url part of WFS can be changed as follows.
TYPESCRIPT
const url = (extent) => `https://example.com/geoserver/wfs?service=WFS&version=2.0.0&request=GetFeature&typename=test:building&srsName=EPSG%3A3857&outputFormat=application%2Fjson&exceptions=application%2Fjson&bbox=${extent[0]}%2C${extent[1]}%2C${extent[2]}%2C${extent[3]}%2CEPSG%3A3857`; const advanced = (extent) => urlBuilder('https://example.com/geoserver/wfs', { service: 'WFS', version: '2.0.0', request: 'GetFeature', typename: 'test:building', srsName: 'EPSG:3857', outputFormat: 'application/json', exceptions: 'application/json', bbox: `${extent.join(',')},EPSG:3857` });
Comparing url and advanced, you can confirm that advanced is much more intuitive.
OpenLayers' VectorLayer object renders the map through a VectorSource object. A vector map isn't simply a picture — since it's rendered in JS as a kind of DOM form, it's a materialized object that can be recognized in the browser.
TYPESCRIPT
import { Vector as VectorLayer } from 'ol/layer'; const wfsLayer = new VectorLayer({ source: wfs, minZoom: 15, zIndex: 5, properties: { name: 'wfs' } });
| Name | Type | Default | Description |
|---|---|---|---|
| className | string | ol-layer | Class name |
| opacity | number | 1 | Opacity (0 ~ 1) |
| visible | boolean | true | Whether visible |
| extent | ol/extent-Extent | undefined | The rendering extent of the layer. Data is not displayed beyond this range | |
| zIndex | number | undefined | Priority (higher is shown on top) | |
| minResolution | number | undefined | Minimum display resolution | |
| maxResolution | number | undefined | Maximum display resolution | |
| minZoom | number | undefined | Minimum display zoom level | |
| maxZoom | number | undefined | Maximum display zoom level | |
| renderOrder | ol/render-OrderFunction | undefined | Sorts the rendering order of Features | |
| renderBuffer | number | 100 | The buffer size of the current area. If the buffer is 100, Features in an area 100 wider than the current area are also rendered |
| source | (ol/source/Vector-VectorSource | ol/source/VectorTile-VectorTile) | undefined | The layer's source | |
| map | ol/PluggableMap-PluggableMap | undefined | Use this layer as an overlay in the specified Map object | |
| declutter | boolean | false | Whether to disable decluttering of the map's images and text |
| style | ol/style/Style-StyleLike | null | undefined | Layer style. If null, only Features with their own style are rendered See ol/style/Style-Style for the default style | |
| background | ol/layer/Base-BackgroundColor | undefined | The layer's background color. Transparent if not specified | |
| updateWhileAnimating | boolean | false | If true, the Feature batch is regenerated during animation. There is a risk of performance degradation if there are many Features If false, the batch is regenerated after the animation finishes |
| updateWhileInteracting | boolean | true | If true, the Feature batch is regenerated during interaction. Similar to the updateWhileAnimating option |
| properties | object | undefined | Arbitrary attributes. Can be manipulated with get(), set() |
You can check the full information about VectorLayer at ol/layer/Vector-VectorLayer.
Create a View object that will declare the map's viewing information.
TYPESCRIPT
import View from 'ol/View'; import proj4 from 'proj4'; const view = new View({ projection: 'EPSG:3857', center: proj4('EPSG:4326', 'EPSG:3857', [ 127.28923267492068, 36.48024986578043 ]), zoom: 17 });
| Name | Type | Default | Description |
|---|---|---|---|
| center | ol/coordinate-Coordinate | undefined | The center of the map | |
| constrainRotation | boolean | number | true | Whether rotation is constrained. If a number, indicates the number of allowed rotation steps (if 0: 90, 180, 270, 360) |
| enableRotation | boolean | true | Whether rotation is enabled |
| extent | ol/extent-Extent | undefined | The map's viewing extent. Cannot go outside the specified range | |
| constrainOnlyCenter | boolean | false | If true, the extent restriction applies only to the View's center, not to the whole extent |
| smoothExtentConstraint | boolean | true | Whether the View can slightly exceed the extent range |
| maxResolution | number | undefined | Maximum viewing resolution. Cannot zoom in beyond the specified resolution. | |
| minResolution | number | undefined | Minimum viewing resolution. Cannot zoom out beyond the specified resolution. | |
| maxZoom | number | 28 | Maximum viewing zoom level. Cannot zoom in beyond the specified zoom level. |
| minZoom | number | 0 | Minimum viewing zoom level. Cannot zoom out beyond the specified zoom level. |
| multiWorld | boolean | false | Whether multiple worlds are used |
| constrainResolution | boolean | false | Whether only integer zoom levels are allowed |
| smoothResolutionConstraint | boolean | true | Whether to use loose zoom in/out rules |
| showFullExtent | boolean | false | Whether to display the entire configured extent |
| projection | ol/proj-ProjectionLike | EPSG:3857 | Coordinate system |
| resolution | number | undefined | Initial resolution | |
| resolutions | Array<number> | undefined | List of available resolutions (descending order). max/minResolution, max/minZoom, zoomFactor options are ignored | |
| rotation | number | 0 | Default rotation value |
| zoom | number | undefined | Default zoom level | |
| zoomFactor | number | 2 | Zoom factor |
| padding | Array<number> | [ 0, 0, 0, 0 ] | Padding |
[ 127.28923267492068, 36.48024986578043 ] is the latitude/longitude (EPSG:4326) coordinates of Sejong City Hall. However, the coordinate system covered in this document is the Google coordinate system (EPSG:3857). Since the coordinate systems differ, conversion is needed accordingly.
Using proj4, you can easily implement coordinate conversion.
TYPESCRIPT
import proj4 from 'proj4'; // Convert lat/long coordinates to EPSG:5179 const xy: number[] = proj4('EPSG:4326', 'EPSG:5179', [ 127.28923267492068, 36.48024986578043 ]);
The above code converts the EPSG:4326 lat/long coordinates of Sejong City Hall to EPSG:5179. In this way, you can convert an existing coordinate system to another coordinate system.
When displaying objects on the map, you can specify that objects be rendered in a desired shape.
The way it's described differs slightly depending on the geospatial data type.
You can specify it on the Layer object to apply it uniformly to all objects included in the layer, or you can specify a style for each Feature.
TYPESCRIPT
// Vector layer object const wfsLayer = new VectorLayer({ source: wfs, // Style can be specified style: {}, minZoom: 15, zIndex: 5, properties: { name: 'wfs' } });
For VectorLayer, you can assign a style object in the options. You can apply it directly in Object form, or use it in callback method form like (feature) => {}.
Unlike the Object form, using the callback method form allows you to write the style variably based on the Feature's data. You could, for example, display each Feature's name or address on a marker, or display different styles based on values.
However, for style branching, it's easier and faster to use the filter option to narrow down to the applicable Features.
For Point data, you can basically write it as below.
TYPESCRIPT
import { Feature } from 'ol'; import Geometry from 'ol/geom/Geometry'; import RenderFeature from 'ol/render/Feature'; import Circle from 'ol/style/Circle'; import Fill from 'ol/style/Fill'; import Stroke from 'ol/style/Stroke'; import Style from 'ol/style/Style'; import Text from 'ol/style/Text'; /** * Method that returns a style * * @param {RenderFeature | Feature<Geometry>} feature: Feature * * @returns {Style} style */ function getStyle(feature: RenderFeature | Feature<Geometry>) { return new Style({ image: new Circle({ stroke: new Stroke({ color: 'rgba(3, 102, 53, 1)', width: 2 }), fill: new Fill({ color: 'rgba(3, 102, 53, 0.6)' }), radius: 20 }), text: new Text({ font: '0.8rem sans-serif', fill: new Fill({ color: 'white' }), stroke: new Stroke({ color: 'rgba(0, 0, 0, 1)', width: 4 }), text: feature.get('address') }) }); }
- image: point style
- stroke: point border
- fill: point background color
- radius: radius
- text: text style
- font: text font
- stroke: text border
- fill: text color
- text: text value
The getStyle method takes a Feature as an argument and returns a style. It can be used in the Layer options like style: (feature) => getStyle(feature).
TYPESCRIPT
import { Feature } from 'ol'; import Geometry from 'ol/geom/Geometry'; import RenderFeature from 'ol/render/Feature'; import { Icon } from 'ol/style'; import Fill from 'ol/style/Fill'; import Stroke from 'ol/style/Stroke'; import Style from 'ol/style/Style'; import Text from 'ol/style/Text'; /** * Method that returns a style * * @param {RenderFeature | Feature<Geometry>} feature: Feature * * @returns {Style} style */ function getStyle(feature: RenderFeature | Feature<Geometry>) { return new Style({ image: new Icon({ src: 'https://t1.daumcdn.net/cfile/tistory/99857F4F5E738F472F', scale: 0.05 }), text: new Text({ font: '0.8rem sans-serif', fill: new Fill({ color: 'white' }), stroke: new Stroke({ color: 'rgba(0, 0, 0, 1)', width: 4 }), text: feature.get('address') }) }); }
Conversely, you can also use the Icon object to use an external image instead of a solid color.
Polygon data is described as below.
TYPESCRIPT
import { Feature } from 'ol'; import Geometry from 'ol/geom/Geometry'; import RenderFeature from 'ol/render/Feature'; import Fill from 'ol/style/Fill'; import Stroke from 'ol/style/Stroke'; import Style from 'ol/style/Style'; import Text from 'ol/style/Text'; /** * Method that returns a style * * @param {RenderFeature | Feature<Geometry>} feature: Feature * * @returns {Style} style */ function getStyle(feature: RenderFeature | Feature<Geometry>) { return new Style({ stroke: new Stroke({ color: 'rgba(100, 149, 237, 1)', width: 2 }), fill: new Fill({ color: 'rgba(100, 149, 237, 0.6)' }), text: new Text({ font: '0.8rem sans-serif', fill: new Fill({ color: 'white' }), stroke: new Stroke({ color: 'rgba(0, 0, 0, 1)', width: 4 }), text: feature.get('address') }) }); }
The image option is excluded, and you can compose the shape's style using the stroke and fill options.
Create the Map object that assembles all the information to build a map.
TYPESCRIPT
import Map from 'ol/Map'; import { Vector as VectorSource } from 'ol/source'; import { GeoJSON } from 'ol/format'; import { bbox } from 'ol/loadingstrategy'; import { Vector as VectorLayer } from 'ol/layer'; import View from 'ol/View'; import proj4 from 'proj4'; import { Feature } from 'ol'; import Geometry from 'ol/geom/Geometry'; import RenderFeature from 'ol/render/Feature'; import Fill from 'ol/style/Fill'; import Stroke from 'ol/style/Stroke'; import Style from 'ol/style/Style'; import Text from 'ol/style/Text'; // WFS vector source const wfs = new VectorSource({ format: new GeoJSON(), url: (extent) => urlBuilder('https://example.com/geoserver/wfs', { service: 'WFS', version: '2.0.0', request: 'GetFeature', typename: 'TEST:buld_sejong', srsName: 'EPSG:3857', outputFormat: 'application/json', exceptions: 'application/json', bbox: `${extent.join(',')},EPSG:3857` }), strategy: bbox }); // Vector layer object const wfsLayer = new VectorLayer({ source: wfs, style: feature => new Style({ stroke: new Stroke({ color: 'rgba(100, 149, 237, 1)', width: 2 }), fill: new Fill({ color: 'rgba(100, 149, 237, 0.6)' }), text: new Text({ font: '0.8rem sans-serif', fill: new Fill({ color: 'white' }), stroke: new Stroke({ color: 'rgba(0, 0, 0, 1)', width: 4 }), text: feature.get('address') }) }), minZoom: 15, zIndex: 5, properties: { name: 'wfs' } }); // View object const view = new View({ projection: 'EPSG:3857', center: proj4('EPSG:4326', 'EPSG:3857', [ 127.28923267492068, 36.48024986578043 ]), zoom: 17 }); // Map object const map = new Map({ layers: [ vworldBaseLayer, vworldHybridLayer, wfsLayer ], target: 'map', view: view });
| Name | Type | Default | Description |
|---|---|---|---|
| controls | ol/Collection-Collection<ol/control/Control-Control> | Array<ol/control/Control-Control> | undefined | ol/control/defaults | The map's control object |
| pixelRatio | number | window.devicePixelRatio | Device pixel ratio |
| interactions | ol/Collection-Collection<ol/interaction/Interaction-Interaction> | Array<ol/interaction/Interaction-Interaction> | undefined | ||
| keyboardEventTarget | HTMLElement | Document | string | undefined | Target element for keyboard events | |
| layers | Array<ol/layer/Base-BaseLayer> | ol/Collection-Collection<ol/layer/Base-BaseLayer> | ol/layer/Group-LayerGroup | undefined | List of layers. The later in the array, the higher the priority | |
| maxTilesLoading | number | 16 | Maximum number of tiles that can load simultaneously |
| moveTolerance | number | 1 | Minimum pixels the mouse must move to be recognized as a map move event |
| overlays | ol/Collection-Collection<ol/Overlay-Overlay> | Array<ol/Overlay-Overlay> | undefined | The map's overlay object | |
| target | HTMLElement | string | undefined | The DOM or DOM id where the map is displayed | |
| view | ol/View-View | Promise<ol/View-View> | undefined | The map's view object |
Assign the objects declared so far to the Map object. The declared map is displayed in the DOM specified in target.
target: map means the map is displayed in the DOM whose id is map. It doesn't have to be an id — you can also assign an HTMLElement.
You can confirm that the data called via WFS is output according to the style it describes.
You can check an example implementing this at OpenLayers6 Sandbox - WFS.
You can confirm that geospatial data is called via GeoServer and rendered onto the map by OpenLayers.
