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!.
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_()