Which Valais Catchments Have I Never Crossed on Foot?

The final answer is simple: recorded walks and hikes have crossed 60 of the 135 Valais hydrological sub-catchments in the federal 40 km² dataset. A catchment is the land area whose precipitation and runoff drain toward a common outlet. The visited catchments cover 2’498 km², or 48 percent of the canton by catchment area. Seventy-five catchments remain untouched in my activity archive.

The path to that answer is the story. A casual question on a Sunday evening produced a confident result within minutes. Two checks then changed it: first the database, then the definition of an eligible activity. The processing chain stayed unchanged throughout and returned the correct result for the data and activity definition supplied at each stage. The answer moved because those inputs changed.

The original exchange

The exchange began on 30 August 2026, lightly edited here for English.

Original prompt

Can you download the hydrological sub-catchments at the 40 km² scale from https://data.geo.admin.ch/browser/index.html#/collections/ch.bafu.wasser-einzugsgebietsgliederung?.language=en, load them into a dedicated table in my qfit GeoPackage, and list the catchments in Valais where I have not yet hiked?

First answer

Done. Here’s the picture:

Loaded into your qfit GeoPackage: new dedicated table ezg_ebene_40km with all 1193 Swiss 40 km² partial catchments (BAFU Topographische Einzugsgebiete, EPSG:2056), plus two flag columns: in_valais (135 catchments) and hike_visited (17). Your original 580 activity tables are untouched.

The answer: 118 of 135 Valais catchments have never seen your hiking boots. You’ve hiked only 16% of Valais by catchment area (859 of 5’210 km²). Your 17 hiked catchments cluster around the Rhône valley floor (Sionne, Borgne, Printse, Dixence, Morge) plus Champex/Ferret side and Trient-adjacent corners.

The excerpt is preserved because its tone matters. The result looked specific and plausible, yet it came from a stale database and a narrow interpretation of hiking.

Finding the right data

The reference data came from the Federal Office for the Environment dataset Topographische Einzugsgebiete Schweizer Gewässer, published as ch.bafu.wasser-einzugsgebietsgliederung on data.geo.admin.ch. The layer of interest was the partial catchments at the 40 km² scale, called bassins versants partiels 40 km² in the French browser interface. It partitions Switzerland and neighbouring areas into 1’193 polygons, each draining toward a common outlet at a consistent hydrological scale.

The geo.admin.ch browser is a STAC client over the federal catalogue. Its collection page renders in JavaScript, while the STAC API exposes the collection metadata, item and downloadable assets directly. The relevant item offered File Geodatabase, GeoPackage, INTERLIS and shapefile formats. The 840 MB GeoPackage archive contained four thematic GeoPackages, including EZG_ebene_40km.gpkg with the 1’193 polygons in LV95 (EPSG:2056), the catchment identifier tezgnr40 and the area in km².

This is a useful catalogue pattern: the portal presents the data and the STAC API provides the machine-readable contract behind it.

The target was qfit_activities.gpkg, the database behind my cycling and hiking analytics. The first file found on the Linux side held 580 activities and only 16 hikes. The production copy on the Windows side stores 1’733 activity rows with full track geometries in one activity_tracks layer, from 1998 to the summer of 2026. It includes 76 Hike and 922 Walk tracks, with no Trekking records.

Extending the GeoPackage schema

The catchments needed a home inside the fitness GeoPackage. A GeoPackage is a SQLite database with standard metadata tables. The corrected schema workflow had four steps:

  • Register EPSG:2056 in gpkg_spatial_ref_sys because the fitness database did not yet contain the Swiss projection.
  • Create the feature table ezg_ebene_40km with the identifier, geometry, catchment number and area columns.
  • Register the table in gpkg_contents with its bounding box and spatial reference.
  • Register the geometry column in gpkg_geometry_columns.

The 1’193 rows then moved across through an SQLite ATTACH of the BAFU file. The first implementation had omitted the spatial-reference registration. ogrinfo caught the mistake when GDAL refused to read the layer, and copying the EPSG:2056 WKT definition from the BAFU GeoPackage corrected it.

The analysis fields extended the schema. in_valais marks catchments whose centroid falls inside the canton polygon. hike_visited, walk_visited and trekking_visited preserve the source categories. on_foot_visited stores their union. Four matching count fields record how many qualifying tracks entered each catchment. The component fields preserve provenance while the combined fields support the map and gap analysis. QGIS and Python can read the result without repeating the spatial join.

The spatial join

Three inputs, one result.

The activity tracks live in WGS84. One ogr2ogr operation reprojected all 1’733 rows from the activity_tracks layer to LV95. The canton boundary came from swissBOUNDARIES3D, also on the federal catalogue, as a polygon layer selected with kantonsnummer = 23.

The join used a spatial index built with Shapely’s STRtree. For every track, candidate catchments came from the index, then an intersection test kept only polygons the track crossed. A visit required more than 100 metres of projected two-dimensional track length inside the catchment, measured in LV95. This excluded boundary touches and short GPS artefacts that placed part of a track across a catchment boundary. The 60/75 result reported here uses this rule.

For a future iteration, I would use an interior-penetration rule instead. Each catchment would first receive a 100-metre inward buffer, represented geometrically as buffer(-100 m). A visit would count when the track intersects this reduced interior polygon. The path would therefore need to penetrate at least 100 metres beyond the catchment boundary at some point, instead of accumulating 100 metres of travel close to that boundary. This criterion expresses the intended meaning of entering a catchment more directly while leaving the rest of the processing chain unchanged.

Valais membership used the catchment centroid, so every border-straddling polygon was assigned to one canton. This produced 135 Valais catchments under a simple, reproducible rule. It remains an approximation: an area-overlap method could assign some border catchments differently.

The result

Sixty of the 135 Valais catchments contain a recorded Hike or Walk, covering 2’498 km² of 5’210 km², or 48 percent. The remaining 75 catchments cover 2’712 km², 52 percent of the canton by catchment area.

The two corrections are visible in the totals. The stale 580-activity copy produced 17 visited and 118 unvisited catchments. The production database raised the hike-only result to 40 visited and 95 unvisited. Adding every recorded Walk produced the final 60/75 split. Walks therefore add twenty catchments beyond the hike-only result.

Valais: visited and never-visited catchments on foot
Map and analysis: Emmanuel Belo. Data sources: Federal Office for the Environment (FOEN/BAFU), Topographic catchment areas of Swiss watercourses; Federal Office of Topography swisstopo, swissBOUNDARIES3D; personal QFit activity archive. Rendered with PyQGIS.

The on-foot map shows a coherent footprint: the Rhône valley core around Sion (Sionne, Borgne, Morge, Printse, Dixence), the Val d’Hérens and Ferpècle side, the Grande Dixence and Moiry country, the Aletsch and Oberaletsch glaciers by way of the Massa, the Salanfe and Emosson dams, the Trient and Eau Noire corner, and the Bagnes and Entremont valleys. Walks extend this footprint into parts of the Vièze, Saaser Vispa, Trient, Dranse and Rhône corridor.

The never-visited list provides a planning backlog for future trips: the Matter Vispa (Zermatt), upper Saaser Vispa (Saas-Fee), Binna (Binntal), Lonza (Lötschenental), upper Navisence (Zinal), Gornera, Saltina (Simplon), Baltschiederbach, and a long tail of high-alpine headwater catchments.

The calls behind the answer

The commands below carried the analysis. They are shortened to the parts that show which geospatial libraries the agent reached and how; paths are trimmed.

The federal catalogue exposes the same metadata the browser renders through its STAC API, so the work began with a plain HTTP call:

collection="ch.bafu.wasser-einzugsgebietsgliederung"
item="wasser-einzugsgebietsgliederung"
base="https://data.geo.admin.ch/api/stac/v0.9/collections"
curl -s "$base/$collection/items/$item" \
  | jq '.assets | keys'

The asset list confirmed the GeoPackage archive before the 840 MB download. GDAL then inspected and reprojected the data without a GIS interface:

# 1'193 catchments; fields: tezgnr40, teilezgflaeche; CRS: EPSG:2056
ogrinfo -so -al EZG_ebene_40km.gpkg
ogr2ogr -t_srs EPSG:2056 tracks_lv95.gpkg qfit_activities.gpkg activity_tracks

A GeoPackage is SQLite underneath, so the catchments moved into the fitness database through SQL:

ATTACH '/home/ebelo/bafu-ezg/EZG_ebene_40km.gpkg' AS bafu;
INSERT INTO main.ezg_ebene_40km (tezgnr40, teilezgflaeche, geom)
SELECT tezgnr40, teilezgflaeche, geom FROM bafu.EZG_ebene_40km;

The spatial join ran in Shapely. An STRtree narrowed each track to its bounding-box candidate catchments, then a precise intersection measured how much track length fell inside each one:

tree = STRtree(catchment_geometries)
for track in tracks_lv95:
    # Query bounding-box candidates, then measure intersections in LV95 metres.
    for i in tree.query(track):
        if track.intersection(catchment_geometries[i]).length > 100:
            mark_visited(track, catchment_ids[i])

The maps came from headless PyQGIS, driven as a library rather than an application:

# launched with QT_QPA_PLATFORM=offscreen
qgs = QgsApplication([], False); qgs.initQgis()
ezg = QgsVectorLayer(f"{gpkg}|layername=ezg_ebene_40km", "catchments", "ogr")
ezg.setRenderer(QgsCategorizedSymbolRenderer(
    'CASE WHEN "in_valais"!=1 THEN \'outside\' '
    'WHEN "on_foot_visited"=1 THEN \'visited\' ELSE \'not visited\' END',
    [QgsRendererCategory('visited', sym_hiked, "Visited on foot"), ...]))
job = QgsMapRendererCustomPainterJob(map_settings, painter)
job.start(); job.waitForFinished()

Curl, jq, GDAL, SQLite, Shapely and PyQGIS were called by the agent in a shell, in that order, with the result of each call feeding the next. That sequence is the substance of agentic geospatial work: standard tools, composed without a human at the keyboard between the calls.

What the workflow changes

The STAC catalogues behind the browser, the GeoPackage standards, GDAL, SQLite, Shapely and PyQGIS form the pipeline, and this article records the source layers, coordinate systems, fields, calls and visit rule needed to reproduce the method. The one-off analysis and rendering scripts have not yet been published.

For public-sector and engineering readers, the enabling conditions are familiar: STAC catalogues with stable APIs, open formats, documented projections and open-source processing. An agent shortens the distance between a question and a testable spatial result, provided the inputs, classification rules and outputs are checked.

This run also shows the role of verification. The agent found the source, modified the database, performed the join and produced the map. It also selected a stale input at first and interpreted hiking too narrowly. The corrected answer emerged through explicit checks of database completeness and activity categories.

The gap list is now a decision instrument. Future hiking plans can start from untouched catchments. The same pattern extends to professional questions: which municipalities have no coverage, which parcels have no inspection, which sub-catchments have no gauging station. In each case, a reference geometry layer is compared with an activity or asset layer, the matching rule is documented, and the gaps are written back into the database as reusable evidence.

Discover more from Digital Pathlines

Subscribe now to keep reading and get access to the full archive.

Continue reading