This lesson is being piloted (Beta version)

Accessing OTN GeoServer Data

Overview

Teaching: 15 min
Exercises: 15 min
Questions
  • What is GeoServer and what does it serve?

  • How do I download OTN layers like stations or receivers?

  • How can I bring these layers into R or Python for mapping?

Objectives
  • Know what kinds of spatial layers OTN publishes via GeoServer.

  • Construct a WFS GetFeature request for a specific layer.

  • Load GeoServer data directly into R or Python for analysis and mapping.

What is GeoServer?

GeoServer is an open-source server that follows international OGC (Open Geospatial Consortium) standards to share spatial data:

How OTN uses it
OTN publishes infrastructure layers through GeoServer — things like:

These layers let researchers:

In short: GeoServer is OTN’s map server.
For reproducible analysis we’ll focus on WFS, since it delivers data in tabular or spatial formats that R and Python can read directly.

Anatomy of a WFS GetFeature request

A WFS request is just a URL with parts you can mix and match:

https://members.oceantrack.org/geoserver/otn/ows?
service=WFS&
version=1.0.0&
request=GetFeature&
typeName=otn:stations_receivers&
outputFormat=csv

What each part means

Base endpoint
https://members.oceantrack.org/geoserver/otn/ows?
service=WFS
specifies the service type (Web Feature Service)
version=1.0.0
version of the WFS standard to use
request=GetFeature
the action to perform: fetch vector features
typeName=otn:stations_receivers
the layer to request
outputFormat=csv
format for the results (CSV, GeoJSON, Shapefile/ZIP)

Optional filters

&bbox=-70,40,-40,60,EPSG:4326

Restrict features to a geographic bounding box (minLon, minLat, maxLon, maxLat, CRS)

&cql_filter=collectioncode='MAST'

Filter features by attribute values (here, only rows where collectioncode = MAST)


OTN GeoServer catalog with otn:stations_receivers highlighted

The screenshot above shows the GeoServer catalog at geoserver.oceantrack.org.
From this interface you can:

Note: by default, downloads are limited to 50 features. To retrieve larger datasets you must adjust the request (e.g. add &maxFeatures=50000).

For reproducible workflows, it is better to build a WFS request URL and load the data programmatically in R or Python (examples below).


Accessing OTN GeoServer Data

Example: Receivers Layer in R

Before we can use spatial layers in analysis, we first need to build the WFS request and then read the results into R. Here’s how:

# If needed, first install:
# install.packages("readr")

library(readr)

# 1) Define WFS URL (CSV output)
# This is the "request URL" that tells GeoServer what we want.
# Here: the `otn:stations_receivers` layer in CSV format.
wfs_csv <- "https://members.oceantrack.org/geoserver/otn/ows?
service=WFS&version=1.0.0&request=GetFeature&
typeName=otn:stations_receivers&outputFormat=csv"

# 2) Download directly into a data frame
# R will fetch the URL and treat the result as a CSV.
receivers <- read_csv(wfs_csv, guess_max = 50000, show_col_types = FALSE)

# 3) Preview first rows
print(head(receivers, 5))

Example: Receivers Layer in Python

The same workflow works in Python: construct the URL, download it, read into a DataFrame.

# If needed, first install:
# pip install pandas

import pandas as pd

# 1) Define WFS URL (CSV output)
# Same request as above, but written in Python.
wfs_csv = (
    "https://members.oceantrack.org/geoserver/otn/ows?"
    "service=WFS&version=1.0.0&request=GetFeature&"
    "typeName=otn:stations_receivers&outputFormat=csv"
)

# 2) Read CSV into DataFrame
receivers = pd.read_csv(wfs_csv)

# 3) Preview first rows
print(receivers.head())

Follow Along: Mapping OTN Data in Python

Now let’s go one step further: visualizing animal detections and station deployments on an interactive map. We’ll use folium to create a Leaflet web map.

1. Imports & Setup

Here we load the libraries and prepare some helpers.

# If needed, first install:
# pip install pandas folium requests
import pandas as pd
import requests
from io import StringIO
import folium
from folium.plugins import MarkerCluster, HeatMap

2. Define Region & Layers

Every request can be limited by a bounding box (min/max longitude/latitude). We also specify which layers we want (animals, stations) and a maximum number of features.

lon_lo, lat_lo, lon_hi, lat_hi = -70.0, 40.0, -40.0, 60.0
srs = "EPSG:4326"

animals_layer  = "otn:animals"
stations_layer = "otn:stations"
max_features_animals  = 200_000
max_features_stations = 50_000

3. Build WFS Requests

The base WFS endpoint stays the same, we just plug in:

BASE = "https://members.oceantrack.org/geoserver/otn/ows"

animals_url = (
    f"{BASE}?service=WFS&version=1.0.0&request=GetFeature"
    f"&typeName={animals_layer}&outputFormat=csv&maxFeatures={max_features_animals}"
    f"&bbox={lon_lo},{lat_lo},{lon_hi},{lat_hi},{srs}"
)

stations_url = (
    f"{BASE}?service=WFS&version=1.0.0&request=GetFeature"
    f"&typeName={stations_layer}&outputFormat=csv&maxFeatures={max_features_stations}"
    f"&bbox={lon_lo},{lat_lo},{lon_hi},{lat_hi},{srs}"
)

print("Animals URL:\n", animals_url)
print("\nStations URL:\n", stations_url)

4. Download CSV Data

We send the request, grab the CSV text, and load it into pandas. Lowercasing the column names makes later handling easier.

animals_csv  = requests.get(animals_url, timeout=180).text
stations_csv = requests.get(stations_url, timeout=180).text

animals  = pd.read_csv(StringIO(animals_csv)).rename(columns=str.lower)
stations = pd.read_csv(StringIO(stations_csv)).rename(columns=str.lower)

print("animals shape:", animals.shape)
print("stations shape:", stations.shape)

animals.head(3)

5. Clean Up Data

Geospatial data often needs cleanup. Here we:

if "datecollected" in animals.columns:
    animals["datecollected"] = pd.to_datetime(animals["datecollected"], errors="coerce")

animals = animals.dropna(subset=["latitude","longitude"]).copy()
stations = stations.dropna(subset=["latitude","longitude"]).copy()

print("after cleanup:", animals.shape, stations.shape)

6. Create Interactive Map

Finally, we build a Leaflet map:

# Center map
if len(animals):
    center = [animals["latitude"].median(), animals["longitude"].median()]
else:
    center = [(lat_lo + lat_hi)/2, (lon_lo + lon_hi)/2]

m = folium.Map(location=center, zoom_start=5, tiles="OpenStreetMap")

# ---- Animals markers ----
mc = MarkerCluster(name="Detections").add_to(m)
sample = animals.sample(min(3000, len(animals)), random_state=42) if len(animals) else animals

vern = "vernacularname" if "vernacularname" in animals.columns else None
sci  = "scientificname"  if "scientificname"  in animals.columns else None
date = "datecollected"   if "datecollected"   in animals.columns else None

for _, r in sample.iterrows():
    sp = (vern and r.get(vern)) or (sci and r.get(sci)) or "Unknown"
    when = (pd.to_datetime(r.get(date)).strftime("%Y-%m-%d %H:%M") if date and pd.notna(r.get(date)) else "")
    popup = f"<b>{sp}</b>" + (f"<br>{when}" if when else "")
    folium.Marker([r["latitude"], r["longitude"]], popup=popup).add_to(mc)

# ---- Heatmap ----
if len(animals):
    HeatMap(animals[["latitude","longitude"]].values.tolist(),
            name="Density heatmap", radius=15, blur=20, min_opacity=0.2).add_to(m)

# ---- Stations ----
fg = folium.FeatureGroup(name="Stations").add_to(m)
name_col = "station_name" if "station_name" in stations.columns else None

for _, r in stations.iterrows():
    tip = r.get(name_col) if name_col and pd.notna(r.get(name_col)) else "(station)"
    folium.CircleMarker([r["latitude"], r["longitude"]], radius=4, tooltip=tip).add_to(fg)

folium.LayerControl().add_to(m)

m.save("ocean_map.html")
print("Saved → ocean_map.html")

Outputs

Animal detections as clustered markers

Figure 1. Animal detections only.

Detections with density heatmap

Figure 2. Detections with density heatmap.

Detections, density heatmap, and stations combined
Figure 3. All layers combined: detections, density heatmap, and stations.

Advanced WFS Request Tips

WFS requests can do more than just typeName + outputFormat.
Here are a few useful extras:

Things to avoid

These options make requests faster, lighter, and more reproducible.


Exercise


Key Points

  • GeoServer shares OTN’s spatial data via open standards (WFS/WMS).

  • WFS requests are just URLs: specify the layer, format, and optional filters.

  • You can load these URLs directly into R or Python for analysis.

  • Keep requests efficient with filters, bounding boxes, and paging.

  • For mapping, folium (Python) or leaflet (R) can turn data into interactive maps.