Data Retrieval: IOER Monitor#

Summary

In this section, we will retrieve and visualize spatial data from the IOER Monitor.

In this section, we retrieve spatial data from the IOER Monitor using the Web Coverage Service (WCS). Specifically, we will access the indicator “Percentage of built-up settlement area and transport space to reference area” (Anteil baulich geprägter Siedlungs- und Verkehrsfläche an Gebietsfläche). This indicator describes the proportion of built-up settlement and transport space in an administrative territory and correlates with soil sealing and open space availability (IOER Monitor 2025).

The IOER Monitor data can be previewed in the geo viewer. The necessary WFS and WCS URLs, along with the unique indicator code (S12RG), can be found under ExportOGC Services.

../_images/094_Verdichtung.jpg

Fig. 6 Densification of the block development in Berlin Friedrichshain. Photo: Jürgen Hohmuth.#

Accessing IOER Monitor Data#

The IOER Monitor allows querying:

To use the API programmatically, you need a personal API key. If you don’t have one yet, refer to the previous section for instructions.

Retrieving IOER Monitor API in Python#

Load dependencies

Before running the workflow, ensure the necessary libraries are installed and imported:

Hide code cell source
# Standard library imports
import json
import os
import sys
import io
from pathlib import Path
from urllib.parse import urlencode

# Third-party imports
import matplotlib.pyplot as plt
import pandas as pd
import geopandas as gp
from IPython.display import display, Markdown
import rasterio
from rasterio.plot import show
from rasterio.mask import mask
from owslib.wcs import WebCoverageService
from lxml import etree

Load additional tools module

Hide code cell source
base_path = Path.cwd().parents[0]
module_path = str(base_path / "py")
if module_path not in sys.path:
    sys.path.append(module_path)
from modules import tools

Define parameters

To access the IOER Monitor data, we define two key parameters:

  • MONITOR_WCS_BASE: IOER Monitor API base endpoint

  • IOERMONITOR_WCS_ID: unique indicator code

MONITOR_WCS_BASE = "https://monitor.ioer.de/monitor_api/user" # API base endpoint
IOERMONITOR_WCS_ID = "S12RG" # Unique indicator code

Define the base path to store output files in this notebook

base_path = Path.cwd().parents[0]
OUTPUT = base_path / "out"

Secure API Key

To store your API key securely, use a dotenv (.env) file. This helps keep sensitive data safe and prevents accidental exposure:

Alternatively, use getpass()

If you don’t want to use an .env file, leave this step and you will be asked to directly enter the password in Jupyter below, by using getpass.getpass().

  1. Create a file named .env in the project root. Typically, .env files are added to .gitignore files to prevent them from being tracked.

  2. Add the following line:

    IOERMONITOR_API_KEY=REPLACE-WITH-YOUR-PASSWORD # replace with your password
    
  1. Load the key in your script:

from dotenv import load_dotenv
load_dotenv(
    Path.cwd().parents[0] / '.env', override=True)

MONITOR_API_KEY = os.getenv('IOERMONITOR_API_KEY')

You can continue without IOER Monitor Key

See Notebook 201 to register your IOER Monitor key. You can continue without an IOER Monitor API key, in which case you will only be able to view cached results below (e.g. for reproduction). If you want to retrieve new data (another region, etc.), register for a trial IOER Monitor key.

if MONITOR_API_KEY is None:
    import getpass
    MONITOR_API_KEY = getpass.getpass("Please enter your IOER Monitor API key")
    if not MONITOR_API_KEY:
        # user response empty
        print("Monitor API key not provided. Continuing with cached results..")

Querying WCS Data#

Configure API request

In order to connect to WCS services from Python, we use owslib (see documentation of owslib.wcs).

from urllib.parse import urlencode

params = {
    "id": IOERMONITOR_WCS_ID,
    "key": MONITOR_API_KEY,
    "service": "wcs",
}
wcs_url = f"{MONITOR_WCS_BASE}?{urlencode(params)}"

wcs = WebCoverageService(wcs_url, version="1.0.0") # WCS version `1.0.0`

Tip

When making requests to web APIs, you often need to pass parameters in a URL. However, some characters (such as spaces, special symbols, or non-ASCII characters) can cause issues if they are not properly encoded. urlencode prevents character encoding issues and improves readability. For more information, see urllib.parse module documentation

Explore Available Data

Let’s first run some checks on the returned wcs object and see what data we can access. The data is available for different time intervals and resolutions, as you can see below.

pd.DataFrame(wcs.contents.keys())
Hide code cell output
0
0 S12RG_2000_100m
1 S12RG_2000_200m
2 S12RG_2000_500m
3 S12RG_2000_1000m
4 S12RG_2000_5000m
... ...
103 S12RG_2023_200m
104 S12RG_2023_500m
105 S12RG_2023_1000m
106 S12RG_2023_5000m
107 S12RG_2023_10000m

108 rows × 1 columns

Select the dataset for 2023 at 200m raster resolution, which leads us to the key S12RG_2023_200m.

LAYER = 'S12RG_2023_200m'

Check the supported output formats for this layer.

if MONITOR_API_KEY: print(wcs.contents[LAYER].supportedFormats)
['GTiff']

We can also query all additional available metadata for the layer (see dropdown below).

Hide code cell source
layer_metadata = wcs.contents[LAYER]

print("Available Attributes for the Layer:")
if not MONITOR_API_KEY:
    print("Skipping because API key is not available.")
else:
    for attr in dir(layer_metadata):
        if not attr.startswith("_"):
            try:
                value = getattr(layer_metadata, attr)
                if attr == "descCov":
                    xml_content = etree.tostring(
                        value, pretty_print=True, encoding="unicode")
                    print(f"{attr} (XML Content):\n{xml_content}")
                else:
                    print(f"{attr}: {value}")
            except Exception as e:
                print(f"{attr}: Error accessing attribute - {e}")
Hide code cell output
Available Attributes for the Layer:
abstract: None
axisDescriptions: []
boundingBox: None
boundingBoxWGS84: (4.93067647168661, 46.8491905772048, 15.9815668414187, 55.5046963165829)
boundingboxes: [{'nativeSrs': 'EPSG:4326', 'bbox': (4.93067647168661, 46.8491905772048, 15.9815668414187, 55.5046963165829)}, {'nativeSrs': 'EPSG:3035', 'bbox': (4000000.0, 2650000.0, 4700000.0, 3600000.0)}]
crsOptions: None
defaulttimeposition: None
grid: <owslib.coverage.wcs100.RectifiedGrid object at 0x7714c64e01a0>
id: S12RG_2023_200m
keywords: []
styles: None
supportedCRS: [urn:ogc:def:crs:EPSG::3035, urn:ogc:def:crs:EPSG::3035]
supportedFormats: ['GTiff']
timelimits: []
timepositions: []
title: S12RG_2023_200m

Check the maximum available boundary for this layer. We can see that the limits are available in two different projections. In the following we will use the projected version of the boundary and not the WGS1984 version.

if MONITOR_API_KEY: print(wcs.contents[LAYER].boundingboxes)
Hide code cell output
[{'nativeSrs': 'EPSG:4326', 'bbox': (4.93067647168661, 46.8491905772048, 15.9815668414187, 55.5046963165829)}, {'nativeSrs': 'EPSG:3035', 'bbox': (4000000.0, 2650000.0, 4700000.0, 3600000.0)}]

Check the coordinate reference system (CRS).

if MONITOR_API_KEY: print(wcs.contents[LAYER].supportedCRS) # ['EPSG:3035']
[urn:ogc:def:crs:EPSG::3035, urn:ogc:def:crs:EPSG::3035]

Retrieve and visualize data

Set up query parameters and request the dataset.

BBOX = None
if MONITOR_API_KEY: BBOX = wcs.contents[LAYER].boundingboxes[1]["bbox"]
CRS = "EPSG:3035"
monitor_param = {
    "identifier": LAYER,
    "bbox": BBOX,
    "resx": 500,
    "resy": 500,
    "crs": CRS,
    "format": "GTiff"
} 
if MONITOR_API_KEY: response = wcs.getCoverage(**monitor_param)
monitor_param
{'identifier': 'S12RG_2023_200m',
 'bbox': (4000000.0, 2650000.0, 4700000.0, 3600000.0),
 'resx': 500,
 'resy': 500,
 'crs': 'EPSG:3035',
 'format': 'GTiff'}

Load and display the GeoTiff with rasterio. If a cache exist, we prefer to load it directly (instead of querying the API again). If it does not exist, write it.

cache_file = OUTPUT / f"{LAYER}_DE.tiff"

if not cache_file.exists():
    if not MONITOR_API_KEY:
        if not Path(OUTPUT / "S12RG_2023_200m_DE.zip").exists():
            tools.get_zip_extract(
                output_path=OUTPUT,
                uri_filename="https://datashare.tu-dresden.de/s/MjDFj4bxoALa2Hz/download")
    else:
        # write to cache
        with open(cache_file, "wb") as f:
            f.write(response.read())

Visualize response (or cache).

with rasterio.open(cache_file) as src:
    fig, ax = plt.subplots(figsize=(8, 8))
    show(src, ax=ax)
    ax.axis('off')
../_images/6ca4e692e1b6ca60f18f3fca3b0708bd8bf8a8632387c94ca40f12a13157afeb.png

Filtering data for Saxony#

However, we want to restrict the raster data to the following boundaries of the state of Saxony, similar to the way we restricted the responses for the GBIF Occurrence API.

  1. Reproject the Saxony boundary to EPSG:3035.

  2. Get boundaries (see section Data Retrieval: GBIF & LAND)

  3. Update the monitor parameters (a Python dictionary) with the new bbox.

  4. Get the grid with the new bbox boundary

Restricting to Saxony boundaries

Load Saxony boundaries and reproject to match WCS layer.

sachsen_proj = gp.read_file(OUTPUT / 'saxony.gpkg')
BBOX = sachsen_proj.bounds.values.squeeze()
monitor_param["bbox"] = list(map(str, BBOX))

Retrieve and visualize clipped data using rasterio.show().

  1. Check and retrieve cache

cache_file = OUTPUT / f"{LAYER}_Saxony.tiff"

if not cache_file.exists():
    if not MONITOR_API_KEY:
        if not Path(OUTPUT / "S12RG_2023_200m_Saxony.zip").exists():
            tools.get_zip_extract(
                output_path=OUTPUT,
                uri_filename="https://datashare.tu-dresden.de/s/Bm74ix6BDQtDzmP/download")
    else:
        # retrieve and write to cache
        response = wcs.getCoverage(**monitor_param)
        with open(cache_file, "wb") as f:
            f.write(response.read())
  1. Visualize

with rasterio.open(cache_file) as src:
    fig, ax = plt.subplots(figsize=(8, 8))
    show(src, ax=ax)
    sachsen_proj.boundary.plot(
        ax=ax, color='white', linewidth=2)
    ax.axis('off')
../_images/8c48c44c9da74d9dad26b86f4b587d310e07a42870cf87baed52bad44e2d2b0d.png

Clipping the raster

Use rasterio.mask to clip the raster with the boundaries of sachsen_proj. In addition, the cmap (a Matplotlib Colormap) is changed to Reds.

with rasterio.open(cache_file) as src:
    out_image, out_transform = mask(
        src, sachsen_proj.geometry, crop=True, filled=False)
    out_meta = src.meta.copy()
    fig, ax = plt.subplots(
        figsize=(8, 8))
    show(
        out_image, 
        transform=out_transform, 
        ax=ax, cmap='Reds')
    sachsen_proj.boundary.plot(
        ax=ax, color='black', linewidth=1)
    ax.axis('off')
../_images/feefdba15208d4c207639feca3b26710fcff92358e7bd644e7e8e8bbd61e7217.png

See also

For a better understanding of the code, see the rasterio documentation.

Save the results to disk as a GeoTIFF. To do this, we first update the clipped raster meta object (out_meta) with the transformation information.

out_meta.update({
    "driver": "GTiff",
    "height": out_image.shape[1],
    "width": out_image.shape[2],
    "transform": out_transform,
    })

Then use rasterio.open() to write the clipped raster.

gtiff_path = OUTPUT / f'saxony_{LAYER}.tif'

with rasterio.open(gtiff_path, "w", **out_meta) as dest:
    dest.write(out_image)

# Get the file size in MB
file_size = gtiff_path.stat().st_size / (1024 * 1024)

print(f"GeoTIFF saved successfully. File size: {file_size:.2f} MB.")
GeoTIFF saved successfully. File size: 0.57 MB.
List of package versions used in this notebook
package python geopandas matplotlib owslib pandas rasterio requests
version 3.13.3 1.0.1 3.10.1 0.33.0 2.2.3 1.4.3 2.32.3