Using WMS services in geoserver and loading it in OpenLayers

No screencast available yet for this post.

Like WFS, WMS (Web Map Service) is a standard protocol the Open Geospatial Consortium maintains. The difference between these two protocols is that WMS, unlike WFS, is not feature-based but image-based, meaning that instead of vectors (json or xml, for example), WMS serves images created (and styled) server-side. It is much more efficient for creating web maps. In this post, we will see how geoserver implements this protocol and how we can adjust our OpenLayers map to consume WMS services instead of WFS services.

  • Let's go back to geoserver and explore how WFS and WMS are implemented. In the browser, go to the geoserver administration page (http://localhost:8080/geoserver) and click on the "Layer Preview" in the "Data" submenu on the left. You can see all published layers and all sharing protocols implemented on this page. So far, we have only used WFS with GeoJSON format. To see the response, you can choose "GeoJSON" in the WFS subsection of the "All formats" list of the world-rivers layer, and a new page will open with the GeoJSON representation (vectors) of this layer:

Switching from WFS to WMS

  • For obvious reasons, the geoserver layer preview page only shows the first 50 features in WFS (have a close look at the URL, there is a maxFeatures=50 query parameter implemented). The first 50 features are, however, more than 35kB in GeoJSON. Now let's go back to layer preview and select PNG in the WMS subsection of the "All formats" list of world-rivers; a new page with the following image (or similar) image should open:

Switching from WFS to WMS

  • With WMS, geoserver shares a small image (png in this case) instead of an enormous vector (json) file; that's what we are looking for. Now let's go back to the resources/js/components/map.js file and try to load the world-rivers layer as WMS instead of WFS in our map:
import Map from "ol/Map.js";
import View from "ol/View.js";
// Imports the ImageLayer object from OpenLayers
import TileLayer from "ol/layer/Tile.js";
import {Image as ImageLayer, Tile as TileLayer} from 'ol/layer.js';
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import OSM from "ol/source/OSM.js";
import { Style, Fill, Stroke, Circle, Text } from "ol/style.js";
import GeoJSON from "ol/format/GeoJSON";
import Overlay from "ol/Overlay.js";
import { bbox as bboxStrategy } from "ol/loadingstrategy.js";
// Imports ImageWMS source from OpenLayers
import ImageWMS from 'ol/source/ImageWMS.js';

document.addEventListener("alpine:init", () => {
    Alpine.data("map", function () {
        return {
            legendOpened: false,
            map: {},
            initComponent() {
                let paramsObj = {
                    servive: "WFS",
                    version: "2.0.0",
                    request: "GetFeature",
                    outputFormat: "application/json",
                    crs: "EPSG:4326",
                    srsName: "EPSG:4326",
                };

                const baseUrl = "http://localhost:8080/geoserver/wfs?";

                let monumentsLayer = new VectorLayer({
                    source: new VectorSource({
                        format: new GeoJSON(),
                        url: (extent) => {
                            paramsObj.typeName = "laravelgis:monuments";
                            paramsObj.bbox = extent.join(",") +  ",EPSG:4326";
                            let urlParams = new URLSearchParams(paramsObj);
                            return baseUrl +  urlParams.toString();
                        },
                        strategy: bboxStrategy,
                    }),
                    style: this.monumentsStyleFunction,
                    label: 'Monuments',
                });

                let worldAdministrativeBoundariesLayer = new VectorLayer({
                    source: new VectorSource({
                        format: new GeoJSON(),
                        url: (extent) => {
                            paramsObj.typeName = "laravelgis:world-administrative-boundaries";
                            paramsObj.bbox = extent.join(",") +  ",EPSG:4326";
                            let urlParams = new URLSearchParams(paramsObj);
                            return baseUrl +  urlParams.toString();
                        },
                        strategy: bboxStrategy,
                    }),
                    style: this.worldAdministrativeBoundariesStyleFunction,
                    label: 'World Administrative Boundaries',
                });

// Completely change the worldRiversVariable
                let worldRiversLayer = new VectorLayer({
                    source: new VectorSource({
                        format: new GeoJSON(),
                        url: (extent) => {
                            paramsObj.typeName = "laravelgis:world-rivers";
                            paramsObj.bbox = extent.join(",") +  ",EPSG:4326";
                            let urlParams = new URLSearchParams(paramsObj);
                            return baseUrl +  urlParams.toString();
                       },
                        strategy: bboxStrategy,
                    }),
                    style: this.worldRiversStyleFunction,
                    label: 'World Rivers',
                });

// We now use an ImageLayer with a ImageWMS source
                let worldRiversLayer = new ImageLayer({
                    source: new ImageWMS({
                      url: 'http://localhost:8080/geoserver/wms',
                      params: {'LAYERS': 'laravelgis:world-rivers'},
                      serverType: 'geoserver',
                    }),
// We keep the label so out legend keeps working
                    label: 'World Rivers',
                });

                this.map = new Map({
                    target: this.$refs.map,
                    layers: [
                        new TileLayer({
                            source: new OSM(),
                            label: 'OpenStreetMap',
                        }),
                        worldAdministrativeBoundariesLayer,
                        worldRiversLayer,
                        monumentsLayer
                    ],
                    view: new View({
                        projection: "EPSG:4326",
                        center: [-78.2161, -0.7022],
                        zoom: 8,
                    }),
                    overlays: [
                        new Overlay({
                            id: 'info',
                            element: this.$refs.popup,
                            stopEvent: true,
                        }),
                    ],
                });

                this.map.on("singleclick", (event) => {
                    if (event.dragging) {
                        return;
                    }

                    let overlay = this.map.getOverlayById('info')
                    overlay.setPosition(undefined)
                    this.$refs.popupContent.innerHTML = ''

                    this.map.forEachFeatureAtPixel(
                        event.pixel,
                        (feature, layer) => {
                            if (layer.get('label') === 'Monuments' && feature) {
                                this.gotoFeature(feature)

                                let content =
                                    '<h4 class="text-gray-500 font-bold">' +
                                    feature.get('name') +
                                    '</h4>'

                                content +=
                                    '<img src="' +
                                    feature.get('image') +
                                    '" class="mt-2 w-full max-h-[200px] rounded-md shadow-md object-contain overflow-clip">'

                                this.$refs.popupContent.innerHTML = content

                                setTimeout(() => {
                                    overlay.setPosition(
                                        feature.getGeometry().getCoordinates()
                                    );
                                }, 500)

                                return
                            }
                        },
                        {
                            hitTolerance: 5,
                        }
                    );
                });
            },
            closePopup() {
                let overlay = this.map.getOverlayById('info')
                overlay.setPosition(undefined)
                this.$refs.popupContent.innerHTML = ''
            },
            monumentsStyleFunction(feature, resolution) {
                return new Style({
                    image: new Circle({
                        radius: 4,
                        fill: new Fill({
                            color: "rgba(0, 255, 255, 1)",
                        }),
                        stroke: new Stroke({
                            color: "rgba(192, 192, 192, 1)",
                            width: 2,
                        }),
                    }),
                    text: new Text({
                        font: "12px sans-serif",
                        textAlign: "left",
                        text: feature.get("name"),
                        offsetY: -15,
                        offsetX: 5,
                        backgroundFill: new Fill({
                            color: "rgba(255, 255, 255, 0.5)",
                        }),
                        backgroundStroke: new Stroke({
                            color: "rgba(227, 227, 227, 1)",
                        }),
                        padding: [5, 2, 2, 5],
                    }),
                });
            },
            worldAdministrativeBoundariesStyleFunction(feature, resolution) {
                return new Style({
                    fill: new Fill({
                        color: "rgba(125, 125, 125, 0.1)",
                    }),
                    stroke: new Stroke({
                        color: "rgba(125, 125, 125, 1)",
                        width: 2,
                    }),
                    text: new Text({
                        font: "16px serif bold",
                        text: feature.get("name"),
                        fill: new Fill({
                            color: "rgba(32, 32, 32, 1)",
                        }),
                    }),
                });
            },
// We don't need the style function for world rivers as WMS are styled on the server
// We will see how to do it in geoserver in the next lesson
            worldRiversStyleFunction(feature, resolution) {
                let text;
                let width = 2;
                if(resolution < 0.002){
                    text = new Text({
                        font: "20px serif",
                        text: feature.get("river_map"),
                        fill: new Fill({
                            color: "rgba(0, 0, 255, 1)",
                        }),
                    });

                    width = 4;
                }

                return new Style({
                    stroke: new Stroke({
                        color: "rgba(0, 0, 255, 1)",
                        width: width,
                    }),
                    text: text,
                });
            },
            gotoFeature(feature) {
                this.map.getView().animate({
                    center: feature.getGeometry().getCoordinates(),
                    zoom: 15,
                    duration: 500,
                });
            },
        };
    });
});
  • If we have a look at our map and investigate the network tab of the browser developers' tools, we can see that the world-rivers layer is now served by geoserver as a png file:

Switching from WFS to WMS

  • If we open this link in a new tab, we can also see that the png image is cropped adequately with the current map extent and that OpenLayers displays it correctly on the map using transparency:

Switching from WFS to WMS

  • We can also pan and zoom out on the map, and OpenLayers automatically queries the WMS service to adjust to the current map extent; we no longer need to load a massive amount of vector data; we are now dynamically loading a small png file.

Switching from WFS to WMS

  • We have seen in a previous article that OpenStreetMap does not serve only one image for the map view but many small square image files. This technique is called Tiling. In many cases, a tiled layer can be more performant than an image layer. Geoserver and OpenLayers support it very well; let's try to implement it with the world-rivers layer in the map. To do so, we will have to map a few minor tweaks to the map.js file like this:
import Map from "ol/Map.js";
import View from "ol/View.js";
// We don't need the ImageLayer anymore
import {Image as ImageLayer, Tile as TileLayer} from 'ol/layer.js';
import {Tile as TileLayer} from 'ol/layer.js';
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import OSM from "ol/source/OSM.js";
import { Style, Fill, Stroke, Circle, Text } from "ol/style.js";
import GeoJSON from "ol/format/GeoJSON";
import Overlay from "ol/Overlay.js";
import { bbox as bboxStrategy } from "ol/loadingstrategy.js";
// Import TileWMS instead of ImageWMS
import ImageWMS from 'ol/source/ImageWMS.js';
import TileWMS from 'ol/source/TileWMS.js';

document.addEventListener("alpine:init", () => {
    Alpine.data("map", function () {
        return {
            legendOpened: false,
            map: {},
            initComponent() {
                let paramsObj = {
                    servive: "WFS",
                    version: "2.0.0",
                    request: "GetFeature",
                    outputFormat: "application/json",
                    crs: "EPSG:4326",
                    srsName: "EPSG:4326",
                };

                const baseUrl = "http://localhost:8080/geoserver/wfs?";

                let monumentsLayer = new VectorLayer({
                    source: new VectorSource({
                        format: new GeoJSON(),
                        url: (extent) => {
                            paramsObj.typeName = "laravelgis:monuments";
                            paramsObj.bbox = extent.join(",") +  ",EPSG:4326";
                            let urlParams = new URLSearchParams(paramsObj);
                            return baseUrl +  urlParams.toString();
                        },
                        strategy: bboxStrategy,
                    }),
                    style: this.monumentsStyleFunction,
                    label: 'Monuments',
                });

                let worldAdministrativeBoundariesLayer = new VectorLayer({
                    source: new VectorSource({
                        format: new GeoJSON(),
                        url: (extent) => {
                            paramsObj.typeName = "laravelgis:world-administrative-boundaries";
                            paramsObj.bbox = extent.join(",") +  ",EPSG:4326";
                            let urlParams = new URLSearchParams(paramsObj);
                            return baseUrl +  urlParams.toString();
                        },
                        strategy: bboxStrategy,
                    }),
                    style: this.worldAdministrativeBoundariesStyleFunction,
                    label: 'World Administrative Boundaries',
                });

                let worldRiversLayer = new ImageLayer({
                   source: new ImageWMS({
                     url: 'http://localhost:8080/geoserver/wms',
                     params: {'LAYERS': 'laravelgis:world-rivers'},
                     serverType: 'geoserver',
                   }),
                   label: 'World Rivers',
                });

// Changes the layer type to TileLayer and the source to TileWMS
// We also add the TILED param and set it to true to the layer's source
                let worldRiversLayer = new TileLayer({
                   source: new TileWMS({
                     url: 'http://localhost:8080/geoserver/wms',
                     params: {'LAYERS': 'laravelgis:world-rivers', 'TILED': true},
                     serverType: 'geoserver',
                   }),
                   label: 'World Rivers',
                });

                this.map = new Map({
                    target: this.$refs.map,
                    layers: [
                        new TileLayer({
                            source: new OSM(),
                            label: 'OpenStreetMap',
                        }),
                        worldAdministrativeBoundariesLayer,
                        worldRiversLayer,
                        monumentsLayer
                    ],
                    view: new View({
                        projection: "EPSG:4326",
                        center: [-78.2161, -0.7022],
                        zoom: 8,
                    }),
                    overlays: [
                        new Overlay({
                            id: 'info',
                            element: this.$refs.popup,
                            stopEvent: true,
                        }),
                    ],
                });
(...)
  • Great, as you can see in the image below, OpenLayers is now making several queries to the WMS service, and the image files are much smaller:

Switching from WFS to WMS

  • Let's inspect one of these queries and open it in a new browser tab; we can see that it loads a single tile; we are now consuming a geoserver tiled layer with OpenLayers:

Switching from WFS to WMS

  • Now let's switch the world-administrative-boundaries layer from WFS to Tiles WMS and witness the symbolizing problem we have introduced:
import Map from "ol/Map.js";
import View from "ol/View.js";
import {Tile as TileLayer} from 'ol/layer.js';
import VectorSource from "ol/source/Vector";
import VectorLayer from "ol/layer/Vector";
import OSM from "ol/source/OSM.js";
import { Style, Fill, Stroke, Circle, Text } from "ol/style.js";
import GeoJSON from "ol/format/GeoJSON";
import Overlay from "ol/Overlay.js";
import { bbox as bboxStrategy } from "ol/loadingstrategy.js";
import TileWMS from 'ol/source/TileWMS.js';

document.addEventListener("alpine:init", () => {
    Alpine.data("map", function () {
        return {
            legendOpened: false,
            map: {},
            initComponent() {
                let paramsObj = {
                    servive: "WFS",
                    version: "2.0.0",
                    request: "GetFeature",
                    outputFormat: "application/json",
                    crs: "EPSG:4326",
                    srsName: "EPSG:4326",
                };

                const baseUrl = "http://localhost:8080/geoserver/wfs?";

                let monumentsLayer = new VectorLayer({
                    source: new VectorSource({
                        format: new GeoJSON(),
                        url: (extent) => {
                            paramsObj.typeName = "laravelgis:monuments";
                            paramsObj.bbox = extent.join(",") +  ",EPSG:4326";
                            let urlParams = new URLSearchParams(paramsObj);
                            return baseUrl +  urlParams.toString();
                        },
                        strategy: bboxStrategy,
                    }),
                    style: this.monumentsStyleFunction,
                    label: 'Monuments',
                });

                let worldAdministrativeBoundariesLayer = new VectorLayer({
                    source: new VectorSource({
                        format: new GeoJSON(),
                        url: (extent) => {
                            paramsObj.typeName = "laravelgis:world-administrative-boundaries";
                            paramsObj.bbox = extent.join(",") +  ",EPSG:4326";
                            let urlParams = new URLSearchParams(paramsObj);
                            return baseUrl +  urlParams.toString();
                        },
                        strategy: bboxStrategy,
                    }),
                    style: this.worldAdministrativeBoundariesStyleFunction,
                    label: 'World Administrative Boundaries',
                });

                let worldAdministrativeBoundariesLayer = new TileLayer({
                    source: new TileWMS({
                        url: 'http://localhost:8080/geoserver/wms',
                        params: {
                            'LAYERS': 'laravelgis:world-administrative-boundaries',
                            'TILED': true
                        },
                        serverType: 'geoserver',
                    }),
                    label: 'World Administrative Boundaries',
                });
								
                let worldRiversLayer = new TileLayer({
                    source: new TileWMS({
                      url: 'http://localhost:8080/geoserver/wms',
                      params: {'LAYERS': 'laravelgis:world-rivers', 'TILED': true},
                      serverType: 'geoserver',
                    }),
                    label: 'World Rivers',
                  });

                this.map = new Map({
                    target: this.$refs.map,
                    layers: [
                        new TileLayer({
                            source: new OSM(),
                            label: 'OpenStreetMap',
                        }),
                        worldAdministrativeBoundariesLayer,
                        worldRiversLayer,
                        monumentsLayer
                    ],
                    view: new View({
                        projection: "EPSG:4326",
                        center: [-78.2161, -0.7022],
                        zoom: 8,
                    }),
                    overlays: [
                        new Overlay({
                            id: 'info',
                            element: this.$refs.popup,
                            stopEvent: true,
                        }),
                    ],
                });

                this.map.on("singleclick", (event) => {
                    if (event.dragging) {
                        return;
                    }

                    let overlay = this.map.getOverlayById('info')
                    overlay.setPosition(undefined)
                    this.$refs.popupContent.innerHTML = ''

                    this.map.forEachFeatureAtPixel(
                        event.pixel,
                        (feature, layer) => {
                            if (layer.get('label') === 'Monuments' && feature) {
                                this.gotoFeature(feature)

                                let content =
                                    '<h4 class="text-gray-500 font-bold">' +
                                    feature.get('name') +
                                    '</h4>'

                                content +=
                                    '<img src="' +
                                    feature.get('image') +
                                    '" class="mt-2 w-full max-h-[200px] rounded-md shadow-md object-contain overflow-clip">'

                                this.$refs.popupContent.innerHTML = content

                                setTimeout(() => {
                                    overlay.setPosition(
                                        feature.getGeometry().getCoordinates()
                                    );
                                }, 500)

                                return
                            }
                        },
                        {
                            hitTolerance: 5,
                        }
                    );
                });
            },
            closePopup() {
                let overlay = this.map.getOverlayById('info')
                overlay.setPosition(undefined)
                this.$refs.popupContent.innerHTML = ''
            },
            monumentsStyleFunction(feature, resolution) {
                return new Style({
                    image: new Circle({
                        radius: 4,
                        fill: new Fill({
                            color: "rgba(0, 255, 255, 1)",
                        }),
                        stroke: new Stroke({
                            color: "rgba(192, 192, 192, 1)",
                            width: 2,
                        }),
                    }),
                    text: new Text({
                        font: "12px sans-serif",
                        textAlign: "left",
                        text: feature.get("name"),
                        offsetY: -15,
                        offsetX: 5,
                        backgroundFill: new Fill({
                            color: "rgba(255, 255, 255, 0.5)",
                        }),
                        backgroundStroke: new Stroke({
                            color: "rgba(227, 227, 227, 1)",
                        }),
                        padding: [5, 2, 2, 5],
                    }),
                });
            },
// We don't need the worldAdministrativeBoundariesStyleFunction anymore, it's cleaning our code quite a lot!
            worldAdministrativeBoundariesStyleFunction(feature, resolution) {
                return new Style({
                    fill: new Fill({
                        color: "rgba(125, 125, 125, 0.1)",
                    }),
                    stroke: new Stroke({
                        color: "rgba(125, 125, 125, 1)",
                        width: 2,
                    }),
                    text: new Text({
                        font: "16px serif bold",
                        text: feature.get("name"),
                        fill: new Fill({
                            color: "rgba(32, 32, 32, 1)",
                        }),
                    }),
                });
            },
            gotoFeature(feature) {
                this.map.getView().animate({
                    center: feature.getGeometry().getCoordinates(),
                    zoom: 15,
                    duration: 500,
                });
            },
        };
    });
});
  • We kept the monument layer with WFS on purpose so that the click still works with the gotoFeature function and the popup; we will explain how we can use WMS and still get this functionality in a future post.
  • Now let's see what problem we introduced on the map:

Switching from WFS to WMS

  • As we can see, it's working; the world-administrative-boundaries layer is now served as Tiled WMS. However, it doesn't have any transparency or symbols. The symbols used for world-rivers and the world-administrative-boundaries are geoserver default styles for lines and polygon layers. It was not so evident to notice with only world-rivers because the default symbols were blue lines (it may differ in your environment). The world-administrative-boundaries use a solid grey color for the fill and a black line for the stroke; this is not what we want. We will address this problem in the next lesson, but we've made good progress as we reduced the data transfer from MBs to KBs; even if the map is at its full extent (world), we reduced the amount of javascript by many lines by removing the styling functions and finally, the legend is still working without any change to the code.

Switching from WFS to WMS

In the next post, we will learn about how we can define styles for WMS layers in geoserver using Styled Layer Descriptor (SLD) which is another Open Geospatial Consortium specification. It should be relatively easy for an experienced programmer as it is based on XML.

The commit for this post is available here: switching-from-wfs-to-wms

First published 1 year ago
Latest update 1 year ago
No comment has been posted yet, start the conversation by posting a comment below.
You need to be signed in to post comments, you can sign in here if you already have an account or register here if you don't.