Geoserver Exploration

This section explores and demonstrates some of the functionality of Geoserver. Unlike some of the other demos contained elsewhere, this section is part-blog and part-demo rather than being an out-and-out demo. This is so it can act as a respository of infornmation/notes for my future self in a single location rather than spread out over multiple blog posts.

What is Geoserver?

Geoserver is a web server to serve maps and data from a range of different sources. This can be data stored within a geospatial database, redirected geospatial services from elsewhere or common geospatial datatypes such as shapefiles and rasters. I was aware of Geoserver and had some limited exposure to Web Map Services (WMS) when at University although this mainly consisted of making requests using OpenLayers. This is the first time I have dedicate time, and gotten hands-on, the server-side.

Data can be served in a variety of formats including pre-rended maps returned as JPEG/PNG or can be output as 'raw' formats (KML, GML GeoJson) for consumption by geospatial tools.

Geoserver is built in Java using the Spring Framework and is responsive and scalable. It implements a number of common standards / protocols such as those detailed in the table below.

Protocol Summary Detail
WMS The Web Map Service protocol defines the http interface for requesting map images from a server. Data can be drawn from multiple sources and combined easily if they each use the WMS protocol.

The output formats available include: PNG, JPEG, SGV, GeoTIFF, PDF, KML.
WFS The Web Feature Service provides a protocol to query geospatial features, style them and to perform CRUD operations.

Features returned can include the feature geometry and attibutes. These can be returned in a variety of formats including: GML, Shapefile, GeoJson and CSV.
WCS The Web Coverage Service provided a protocol for accessing raster data (a WFS for raster data).

Data formats returned include: JPEG, GIF, PNG, BMP, Tiff and GeoTIFF.

Installation and Setup

I have limited experience with Java and even less with the Spring Framework / Tomcat, however the installation of Geoserver is a relatively easy process aided by the availability of pre-built and configured docker images. The system can be parameterized so config options can be set easily without having to rebuild the docker images.

I found the installation easy - I simply added a new section to my existing docker compose setup for this site. I opted to not deploy on a separate server to save money.

Geoserver has a built in admininstration area UI where different settings of the site can be specified (along with those added as parameters to the docker image). Configuration is pretty straightforward and self-explanatory. Unlike Django, it does NOT use a backend database to store records of the geospatial catalogue and configuration, instead these are stored as a series of XML files. There may be a way to use postgres to do this instead here but I did not look at this in detail - would perhaps be useful if the catalogue of datasets is very big.

There's not much to say about the setup. The documents are relatively clear and there are tutorials to take you through the different setup requried. The admin, aside from settings, is simply a catalogue of data sources to be served where one groups these together, provides any required metadata, indicates the source (db, shapefiled etc.) and the services available. There is an access rights system although I have not looked at this in detail yet.

One area that was not immediately clear was the defintiions of Workspaces, sources and layers/layer groups. A summary of these is provided in the table below.

Feature Summary Detail
Workspace A workspace is a grouping of data stores that are related in someway. When defining the workspace a URI is provided so this needs to be a unique endpoint for querying the service.

The services available (WMS, WFS etc.) are defined on a workspace basis.
Data Store This specifies the connector between a workspace and a given source of data. It may specify a postgis database, shapefile or forwarding WMS. A single workspace can have multiple stores.

A store for postgis would be for the entire db (can specify schema) whereas each shapefile requires it's own store.
Layer A layer is the same as one would typically come across in GIS software. It is a set of features.
Layer Group A layer group allows certain layers to be displayed together and specifies their ordering

Finally on installation, there were a couple of areas where I needed to Google things as part of the setup. These are provided as a reminder below.

When creating datastores for a postgis database in a docker on the same machine "localhost" does not work. Instead use the default Docker host address (172.17.0.1).

There is no option through the UI to copy data to the server for use in a datastore - for instance if one wanted to upload and serve a shapefile. There is an extention for this purpose but I have not tried this yet (here).

As this is a demo site and I don't have much data I simply copied up to the server and then used the docker copy command to place the data in the correct location inside the relevant docker volume as below (countries being the workspace and shapefile name in this instance).


                            
                            e.g. docker cp countries.shp geoserver_container:/opt/geoserver_data/data/countries/
                            
                            
                        

Addendum: A slow lingering death by HTTPS

The only challenging area of the configuration related to the use of HTTPS and getting things set up with a reverse proxy (Nginx). This seems to be an area where others have also struggled. There are plenty of stack overflow and reddit posts on this. I initially opted for HTTP only to avoid the hassle, however this caused issues when I wanted to serve data with MapBox - understandably rejecting mixed HTTPS/HTTP calls.

I struggled for some time with the official Geoserver docker image but could not get this working. Eventually I switched to use the Kartoza image (link below) which has a flag and clearer instructions specifically for my setup (HTTPS to Nginx then HTTP in the backend to geoserver). Given this was a faff I enclose details below of the config below.
Nginx reverse proxy

    
    upstream core {
        server app:8000;
    }
    server {
        listen                      80;  
        server_name                 fearnought.club www.fearnought.club; 
        return                      301     https://$host$request_uri;

    }
    server {
        listen                      443 ssl;
        server_name                 fearnought.club www.fearnought.club; 
        ssl_certificate /....***.pem;
        ssl_certificate_key /.... ****.pem;

        location / {
            proxy_pass              http://core;  
            proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header        Host $host;
            proxy_redirect          off;
            proxy_set_header        X-Real-IP $remote_addr;
            proxy_set_header        X-Forwarded-Proto $scheme;
        }
        location /static/ {
            alias                   /vol/static/;
        }

        location ^~ /geoserver/ {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Proto http;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://myserver_ip:8080/geoserver/;    

        }
    }
    
    
Docker Compose

    
    geoserver:
        image: kartoza/geoserver:2.27.1
        container_name: gs_container
        ports:
            - 8080:8080 
        restart: unless-stopped
        environment:
            - SAMPLE_DATA=true
            - GEOSERVER_DATA_DIR=/opt/geoserver/data
            - ENABLE_JSONP=true
            - GEOSERVER_ADMIN_PASSWORD=${ENV_PASS}
            - GEOSERVER_ADMIN_USER=${ENV_USER}
            - HTTP_PROXY_NAME=fearnought.club
            - HTTP_SCHEME=https
        volumes:
            - geoserver_data:/opt/geoserver 
            - ./additional_libs:/opt/additional_libs:Z 
    

Links

OSGeo introduction to Geoserver
GeoServer (https://geoserver.org/)
Geoserver guidance on docker
Geoserver docker github
Kartoza alternate docker image


Demo

The rest of this section below explores some of the basic functionality of the Geoserver. Making basic requests to some of the services on offer. I'll likely do detailed demos on specific services / api in due course.

WMS GetCapabilities

The GetCapabilities endpoint provides details about the services, metadata and details of the layers available. Note that the response can be quite large and will include a list of all available spatial refrence sysems. This can be addressed by limiting the srs available for the workspace via the admin ui (WMS -> select workspace -> limit srs).

    
payload = {
    "service": "wms",
    "version2": "1.3.0",
    "request": "GetCapabilities",
    "namespace": "xmlns(myprefix=https://fearnought.club/geoserver/sheffield)", 
}

url = "https://fearnought.club/geoserver/fearnought_sheffield/wms"

r = requests.get(url, params=payload)

context['xml_response'] = r.text
       

WMS GetMap

The "GetMap" endpoint returns a pre-rendered map in a variety of outputs (e.g. png, jpeg, pdf). Easy enough, although the bounding box is mandatory so we need to call "GetCapabilities" first and then parse the returned XML in order to the find the bbox values.

"should just take 5 mins - will be like json, maybe even easier" he said, having never worked with XML before.....

Three hours later...ok so we've been on a merry dance looking into XML schemas and hey presto! Got there in the end with a little help from chatGPT - first time it's actually been useful for me. Will take that as a massive W!.

Fun times to parse the xml using a schema.
    
    import xml.etree.ElementTree as ET
    schema = {'wms': 'http://www.opengis.net/wms'}   
    root = ET.fromstring(xml_content)
    layers = root.findall('.//wms:Layer/wms:Layer', schema)
    ## then for each layer find the bboxes..
    bbox_els = layer.findall('wms:BoundingBox', namespaces)
       
The standard request in the view and template snippet
    
    payload = {
        "service": "WMS",
        "version": "1.3.0",
        "request": "GetMap",
        "layers": "fearnought_enschede:geo_enschedeboundary",
        "bbox": "248635.984375,464779.15625,263820.15625,478458.59375",
        "styles": "",
        "width": "300",
        "height": "150",
        "srs": "EPSG:28992",
        "format": "image/png",
    }

    url = "https://fearnought.club/geoserver/fearnought_enschede/wms"
    r = requests.get(url, params=payload)

    context["image"] = b64encode(r.content).decode()

    ## In the template 
    <img src="data:image/png;base64,">
       

WFS GetFeature

In this example the Web Feature Service (WFS) is used. The GetFeature endpoint is called to get the boundary for Enschede. This is transformed to WGS84 to be overlaid on the basemap.

The WMS GetCapabilities returns two sets of bounding box - the native using the spatial reference of the data and the transformed into WG84. This was parsed to get the relevant bbox and then used to set the focus of the map.

    
    payload = {
        "service": "wfs",
        "version2": "2.0.0",
        "request": "GetFeature",
        "typeNames": "fearnought_enschede:geo_enschedeboundary",
        "srsName": "EPSG:4326",
        "outputFormat": "application/json",
    }

    url = "https://fearnought.club/geoserver/fearnought_enschede/wfs"

    # make the request and parse the returned json
    r = requests.get(url, params=payload)
    parsed = json.loads(r.content)

    # setup a map and use bounding box to fit
    m = folium.Map(zoom_start=3)
    m.fit_bounds([[mybbox[1], mybbox[0]], [mybbox[3], mybbox[2]]])

    folium.GeoJson(parsed).add_to(m)
    context["map"] = m._repr_html_()