blog.itcode.devblog.itcode.dev

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.

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.
RWB0104
@RWBwritten at 2022-05-14 17:48:50
A Guide for Developers Traveling Through OpenLayers

시리즈 모아보기

A Guide for Developers Traveling Through OpenLayers

15 / 23

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
ParameterExampleRequireDescription
serviceWFS (fixed)YService name
version2.0.0 (default), 1.1.0, 1.0.0YVersion
requestGetFeature (fixed)YRequest name
typenamerepo_name:layer_nameYLayer name (multiple separated by commas)
srsNameEPSG:4326Base coordinate system (if left empty, the layer's default coordinate system is used)
outputFormatapplication/vnd.ogc.se_xml (default)Response format
exceptionsapplication/vnd.ogc.se_xml (default)Exception response format
propertyNameAll columnsColumn names to include in the response (multiple separated by commas)
bboxxmin,ymin,xmax,ymaxx_{min},y_{min},x_{max},y_{max},EPSG:0000Range 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.

NameTypeDefaultDescription
attributionsol/source/Source-AttributionLike | undefinedAttribution text (bottom-right of map)
featuresArray<ol/Feature-Feature> | ol/Collection-Collection<ol/Feature-Feature> | undefinedFeature array
formatol/format/Feature-FeatureFormat | undefinedThe format used by the URL data loader to recognize data. Required if url is set
loaderol/featureloader-FeatureLoader | undefinedLoader 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
overlapsbooleantrueHow overlapping geometry is handled.
If false, the renderer optimizes the geometry's boundary and fill work
strategyol/source/Vector-LoadingStrategy | undefinedThe data rendering strategy. By default, it uses ol/loadingstrategy.all, which loads all Features at once
urlstring | ol/featureloader-FeatureUrlFunction | undefinedData URL
useSpatialIndexbooleantrueWhether to use a spatial index. If features change frequently or there are few of them, setting this to false improves speed.
wrapXbooleantrueWhether 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' }
});
NameTypeDefaultDescription
classNamestringol-layerClass name
opacitynumber1Opacity (0 ~ 1)
visiblebooleantrueWhether visible
extentol/extent-Extent | undefinedThe rendering extent of the layer. Data is not displayed beyond this range
zIndexnumber | undefinedPriority (higher is shown on top)
minResolutionnumber | undefinedMinimum display resolution
maxResolutionnumber | undefinedMaximum display resolution
minZoomnumber | undefinedMinimum display zoom level
maxZoomnumber | undefinedMaximum display zoom level
renderOrderol/render-OrderFunction | undefinedSorts the rendering order of Features
renderBuffernumber100The 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) | undefinedThe layer's source
mapol/PluggableMap-PluggableMap | undefinedUse this layer as an overlay in the specified Map object
declutterbooleanfalseWhether to disable decluttering of the map's images and text
styleol/style/Style-StyleLike | null | undefinedLayer style. If null, only Features with their own style are rendered
See ol/style/Style-Style for the default style
backgroundol/layer/Base-BackgroundColor | undefinedThe layer's background color. Transparent if not specified
updateWhileAnimatingbooleanfalseIf 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
updateWhileInteractingbooleantrueIf true, the Feature batch is regenerated during interaction. Similar to the updateWhileAnimating option
propertiesobject | undefinedArbitrary 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
});
NameTypeDefaultDescription
centerol/coordinate-Coordinate | undefinedThe center of the map
constrainRotationboolean | numbertrueWhether rotation is constrained. If a number, indicates the number of allowed rotation steps (if 0: 90, 180, 270, 360)
enableRotationbooleantrueWhether rotation is enabled
extentol/extent-Extent | undefinedThe map's viewing extent. Cannot go outside the specified range
constrainOnlyCenterbooleanfalseIf true, the extent restriction applies only to the View's center, not to the whole extent
smoothExtentConstraintbooleantrueWhether the View can slightly exceed the extent range
maxResolutionnumber | undefinedMaximum viewing resolution. Cannot zoom in beyond the specified resolution.
minResolutionnumber | undefinedMinimum viewing resolution. Cannot zoom out beyond the specified resolution.
maxZoomnumber28Maximum viewing zoom level. Cannot zoom in beyond the specified zoom level.
minZoomnumber0Minimum viewing zoom level. Cannot zoom out beyond the specified zoom level.
multiWorldbooleanfalseWhether multiple worlds are used
constrainResolutionbooleanfalseWhether only integer zoom levels are allowed
smoothResolutionConstraintbooleantrueWhether to use loose zoom in/out rules
showFullExtentbooleanfalseWhether to display the entire configured extent
projectionol/proj-ProjectionLikeEPSG:3857Coordinate system
resolutionnumber | undefinedInitial resolution
resolutionsArray<number> | undefinedList of available resolutions (descending order). max/minResolution, max/minZoom, zoomFactor options are ignored
rotationnumber0Default rotation value
zoomnumber | undefinedDefault zoom level
zoomFactornumber2Zoom factor
paddingArray<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
});
NameTypeDefaultDescription
controlsol/Collection-Collection<ol/control/Control-Control> | Array<ol/control/Control-Control> | undefinedol/control/defaultsThe map's control object
pixelRationumberwindow.devicePixelRatioDevice pixel ratio
interactionsol/Collection-Collection<ol/interaction/Interaction-Interaction> | Array<ol/interaction/Interaction-Interaction> | undefined
keyboardEventTargetHTMLElement | Document | string | undefinedTarget element for keyboard events
layersArray<ol/layer/Base-BaseLayer> | ol/Collection-Collection<ol/layer/Base-BaseLayer> | ol/layer/Group-LayerGroup | undefinedList of layers. The later in the array, the higher the priority
maxTilesLoadingnumber16Maximum number of tiles that can load simultaneously
moveTolerancenumber1Minimum pixels the mouse must move to be recognized as a map move event
overlaysol/Collection-Collection<ol/Overlay-Overlay> | Array<ol/Overlay-Overlay> | undefinedThe map's overlay object
targetHTMLElement | string | undefinedThe DOM or DOM id where the map is displayed
viewol/View-View | Promise<ol/View-View> | undefinedThe 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.

# GIS# GeoServer# OpenLayers# OGC# WFS
ship
blog.itcode.dev

Notes from the π-th Alpaca

7.0.1
Developed by RWB since 2021.057th upgraded at 2026.08