A Guide for Developers Traveling Through OpenLayers - 19. Attaching a Popup to WMS
A Guide for Developers Traveling Through OpenLayers - 19. Attaching a Popup to WMS
Can a popup be shown for an image-based map like WMS, rather than an object-based map like WFS?
On the surface, it seems impossible. In the case of WFS, since the script itself holds the spatial information, we were able to make use of it appropriately to display the desired information. But in the case of WMS, the foundation itself is an image, so its usefulness as analyzable data is very limited.
In other words, a computer can't directly know which objects this single image contains, or how many.
So is it impossible for a WMS map to show a popup?
Something worth considering here is a protocol called GetFeatureInfo. GetFeatureInfo provides information about the Features at a position calculated from the current map's BBOX, the clicked coordinate, and the pixel value on the map.
In other words, by clicking on the map image, we can find out whether a Feature exists and extract that Feature's information. We can use this to implement a popup.
We use the WMS map covered in Chapter 1 as-is.
OpenLayers can display any HTML tag on top of the map through the Overlay object. Although OpenLayers handles the behavior, it isn't an object rendered onto the canvas; it outputs an actual DOM element.
By default, Overlay follows the position clicked on the map. In other words, if you display an Overlay at a specific position on the map and then move the map, the position isn't fixed — it follows the marker. Handling this kind of behavior directly in the DOM would require quite cumbersome handling. Thanks to this characteristic, it's a very good fit for popups.
TXT
GET https://example.com/geoserver/wms?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetFeatureInfo&FORMAT=image%2Fpng&TRANSPARENT=true&QUERY_LAYERS=test:building&layers=buld_sejong&exceptions=application%2Fjson&INFO_FORMAT=application%2Fjson&I=221&J=178&WIDTH=256&HEIGHT=256&CRS=EPSG%3A3857&STYLES=&BBOX=14169590.555392835%2C4366694.551875548%2C14169896.303505976%2C4367000.299988689
| Parameter | Example | Require | Description |
|---|---|---|---|
| service | WMS (fixed) | Y | Service name |
| version | 1.3.0 (fixed), 1.1.1, 1.1.0, 1.0.0 | Y | Version |
| request | GetFeatureInfo (fixed) | Y | Request name |
| layers | repo_name:layer_name | Y | Layer name (separate multiple with commas) |
| styles | style1 | Style name to apply (meaningless for GetFeatureInfo) | |
| crs(or srs) | EPSG:4326 | Reference coordinate system (if empty, uses the layer's default CRS) | |
| bbox | Y | Image extent coordinates | |
| width | 256 | Y | Image width |
| height | 256 | Y | Image height |
| query_layers | repo_name:layer_name | Y | Additional target layer name (separate multiple with commas) |
| info_format | application/vnd.ogc.se_xml (default) | Response format | |
| feature_count | 1 (default) | Maximum number of objects to retrieve | |
| x(or i) | 225 | Y | Pixel x value on the map |
| y(or j) | 156 | Y | Pixel y value on the map |
| exceptions | application/vnd.ogc.se_xml (default) | Exception response format |
GetFeatureInfo, like GetImage, requires quite a lot of parameters. Since GetFeatureInfo itself is an API that returns the marker information at the clicked location, you must provide the clicked position on the map in the form of x, y. In many ways it has a fair number of tricky parameters.
Fortunately, just like with GetImage, OpenLayers provides an object that generates the GetFeatureInfo URL, so you can use it to call it easily. This method is described in the section on handling the Overlay's events. For now, just know that it can be called this way as a plain URL.
Let's create the Overlay object directly.
| Parameter | Type | Default | Description |
|---|---|---|---|
| id | number | string | undefined | Overlay ID Used when calling the ol/Map-Map#getOverlayById method | |
| element | HTMLElement | undefined | The target Element for the overlay | |
| offset | Array<number> | [ 0, 0 ] | Offset (px) for the overlay's display [ x, y ], where x is horizontal and y is vertical. Larger values move it right and down, respectively. |
| position | ol/events/condition-Condition | undefined | Display position of the overlay | |
| positioning | ol/OverlayPositioning | top-left | Placement anchor of the overlay Values such as bottom-left, top-right, etc. |
| stopEvent | boolean | true | Whether to stop event propagation If true, placed in a DOM with the class ol-overlay container-stopevent If false, placed in a DOM with the class specified in the className property |
| insertFirst | boolean | true | Whether to insert the overlay first or append it to the element |
| autoPan | ol/Overlay-PanIntoViewOptions | boolean | false | When setPosition is called on the overlay, automatically pans the map so the overlay is fully visible |
| autoPanAnimation | ol/Overlay-PanOptions | Animation settings for the pan triggered by autoPan. Ignored if the autoPan setting is not an object but a boolean | |
| autoPanMargin | number | 20 | Margin between the overlay and the map edge during the pan triggered by autoPan Ignored if the autoPan setting is not an object but a boolean |
| autoPanOptions | ol/Overlay-PanIntoViewOptions | undefined | Options for autoPan Takes precedence over autoPanAnimation and autoPanMargin | |
| className | string | ol-overlay-container ol-selectable | CSS class value |
The way to create one is as follows.
HTML
<div id="map-popup"></div>
Enter the DOM element to use for the popup. Use the id or class attribute appropriately to identify the DOM. The position of the tag within the HTML code doesn't matter much.
TYPESCRIPT
import { Overlay } from 'ol'; const popup = document.getElementById('map-popup') as HTMLElement | null; const overlay = new Overlay({ id: 'popup', element: popup || undefined, positioning: 'center-center', autoPan: { animation: { duration: 250 } } });
Once you assign the tag and configure it appropriately, you can create the Overlay object.
The complete information for Overlay can be found in the official documentation.
Let's apply the created Overlay to the Map.
TYPESCRIPT
import { Map } from 'ol'; const map = new Map({ // ... overlays: [ overlay ] // ... });
You can apply it as shown above. You can register Overlays in the form of an array.
TYPESCRIPT
// Add an overlay map.addOverlay(); // Return an overlay by ID map.getOverlayById(); // Return the list of overlays map.getOverlays(); // Remove an overlay map.removeOverlay();
The related methods are as above. They can be called on the created Map object.
From here on, the approach differs a bit between WFS and WMS. In the case of WFS, we could access the GetFeature information directly in the script and immediately show the Feature's information.
However, as mentioned repeatedly, WMS's GetImage renders and returns an image based on the spatial information, so there is a limit to how directly it can show Feature information.
By using GetFeatureInfo on click, we can retrieve the Feature data at the clicked location, so let's make use of that.
TYPESCRIPT
import ImageLayer from 'ol/layer/Image'; const layer = new TileLayer({ source: source, minZoom: 15, properties: { name: 'wms' }, zIndex: 5 });
Assume the WMS layer has been declared as above.
TSX
map.on('singleclick', (e) => { // Extract the layer whose WMS properties name is wms const wmsLayer = map.getAllLayers().filter(layer => layer.get('name') === 'wms')[0]; // Call the WMS layer's Source const source: TileWMS | ImageWMS = wmsLayer.getSource(); // Generate the GetFeatureInfo URL const url = source.getFeatureInfoUrl(e.coordinate, map.getView().getResolution() || 0, 'EPSG:3857', { QUERY_LAYERS: 'test:building', INFO_FORMAT: 'application/json' }); // If the GetFeatureInfo URL is valid if (url) { const request = await fetch(url.toString(), { method: 'GET' }).catch(e => alert(e.message)); // If the response is valid if (request) { // If the response is OK if (request.ok) { const json = await request.json(); // If there are no objects if (json.features.length === 0) { overlay.setPosition(undefined); } // If there are objects else { // Create a Feature from the GeoJSON const feature = new GeoJSON().readFeature(json.features[0]); // Create a VectorSource with the created Feature const vector = new VectorSource({ features: [ feature ] }); setPopupState( <ul> <li>{feature.getId() || ''}</li> <li>{feature.get('buld_nm') || <span>No name</span>}</li> <li>{feature.get('bul_man_no')}</li> </ul> ); overlay.setPosition(getCenter(vector.getExtent())); } } // Otherwise else { alert(request.status); } } } });
We make appropriate use of the click event as shown above.
- On click, call the WMS layer.
- Call the Source object from the WMS layer.
- Generate the GetFeatureInfo URL through the Source object's getFeatureInfoUrl method.
- Call GetFeatureInfo on GeoServer.
- Build a Feature from the response's GeoJSON and create a VectorSource object.
- Retrieve and call the desired data from the created Feature object.
- Calculate the data's actual position via vector.getExtent() and set it as the Overlay's position.
The logic proceeds as described above. Of course this is just one example of usage, so you can describe whatever behavior you want in the event to perform various actions.
You can check an example implementing this at OpenLayers6 Sandbox - WMS Popup.
