Skip to content

S3: Health Facility Accessibility — Clermont-Ferrand

Advanced CLI + Map

Capabilities: filter isochrone classify_by_ring

Use Case

A health planner analyzes healthcare coverage in Clermont-Ferrand with four concentric walking isochrones computed on the real BD TOPO road network — 500 m (~5 min), 750 m (~7.5 min), 1 km (~10 min), 1.5 km (~15 min) from every categorie == 'Santé' POI. The four rings are emitted in one multi-source Dijkstra pass (cost_budgets parameter, metric CRS EPSG:2154), then classify_by_ring tags every building with the smallest ring that contains it. Palette: green (inside the 500 m zone, served) → yellow (500-750 m, +2.5 min) → orange (750 m-1 km, +5 min) → red (1-1.5 km, +10 min) → dark red (beyond 1.5 km).

IGN BD TOPO V3 + OSM Data

LayerContentFeaturesSource
routesRoad segments (type, lanes, width, speed)2,272data.geopf.fr — BDTOPO_V3:troncon_de_route
equipementsOSM healthcare POIs (Overpass) — exhaustive: pharmacies, GPs and specialists, hospitals, clinics, dentists, labs, physios, nursing homes, social care, veterinaries...223overpass-api.de — `amenity~hospital
batimentsBuildings to assign~77,000data.geopf.fr — BDTOPO_V3:batiment

Why OSM instead of BD TOPO equipements?

BD TOPO V3 only lists 47 "Santé" establishments in the Clermont bbox — hospitals, clinics, nursing homes, thermal baths. Individual GPs, pharmacies, dentists, labs are absent. OSM surfaces 223 health POIs — Overpass cache generated by scripts/fetch_health_pois_osm.py and committed to examples/datasets/clermont_ferrand_health_osm.geojson.

bash
python examples/prepare_playground_data.py --city clermont-ferrand
gispulse info examples/datasets/clermont_ferrand_bdtopo.gpkg

Pipeline (3 steps)

equipements (223 OSM POIs) ──► filter categorie == 'Santé'
                              │  → 223 health POIs (pharmacies, GPs, labs, nursing homes, hospitals...)


                     isochrone cost_budgets=[500, 750, 1000, 1500]
                              │  Multi-source Dijkstra, EPSG:2154, ONE pass → 4 rings
                              │  → GeoDataFrame of 4 polygons { cost_budget, geometry }

batiments ─────────────────► classify_by_ring ref_layers=[isochrone_rings]
                              │  → access_ring (500|750|1000|1500|99999)
                              │  → access_class (1..5)
                              │  → access_color (green → dark red)

                     ALL buildings mapped by the ring that contains them

Rules

json
{
  "version": 2,
  "name": "health_accessibility",
  "ref_layers": {
    "routes": "routes",
    "batiments": "batiments"
  },
  "steps": [
    {
      "id": "filter_sante",
      "type": "capability",
      "capability": "filter",
      "params": { "expression": "categorie == 'Santé'" }
    },
    {
      "id": "isochrone_rings",
      "type": "capability",
      "capability": "isochrone",
      "params": {
        "ref_layer": "routes",
        "cost_budgets": [500, 750, 1000, 1500],
        "crs_meters": "EPSG:2154",
        "edge_buffer_m": 200,
        "dissolve": true
      },
      "input": "filter_sante"
    },
    {
      "id": "classify_by_ring",
      "type": "capability",
      "capability": "classify_by_ring",
      "params": {
        "ref_layers": ["isochrone_rings"],
        "ring_field": "cost_budget",
        "class_col": "access_class",
        "color_col": "access_color",
        "value_col": "access_ring",
        "palette": ["#1a9850", "#fee08b", "#fdae61", "#f46d43", "#a50026"],
        "use_centroid": true,
        "ring_simplify_tolerance": 10.0
      },
      "input": "batiments"
    }
  ]
}

Multi-budget mode (cost_budgets)

A single Dijkstra pass with cutoff = max(cost_budgets), then per-budget filtering of reachable nodes to buffer+dissolve each. N-ring cost ≈ single-ring cost. Every ring is a filled zone (not a hollow annulus) — classify_by_ring picks the innermost ring that contains each feature. The playground assigns one colour per budget (green → red) and reverses draw order so the smallest ring sits on top.

Perf — use_centroid + ring_simplify_tolerance

On ~50 000 buildings × 4 BD TOPO isochrones the strict polygon-vs-polygon sjoin takes > 100 s: every 1.5 km ring is a unary-union polygon with tens of thousands of vertices, and intersects pays the boundary cost against each building footprint. Two cumulative knobs:

  • use_centroid: true — switches to a within query on each building's centroid. Footprints are tiny (~10-30 m on a side), so class assignment is preserved except for the rare buildings that genuinely straddle a ring boundary.
  • ring_simplify_tolerance: 10.0 — simplifies the rings to 10 m before the join, dropping most vertices without visibly shifting boundaries at city scale.

Typical effect: 138 s → ~3 s on the S3 scenario without changing the displayed classes.

Metric CRS — why EPSG:2154

Source data is in EPSG:4326 (degrees). A cost_budget: 500 without reprojection would be interpreted in degrees (~55 km). The crs_meters: "EPSG:2154" parameter reprojects the network to Lambert-93 for the routing pass, so budgets are expressed in real meters; the result is then reprojected back to the source CRS.

Execution

bash
gispulse run examples/datasets/clermont_ferrand_bdtopo.gpkg \
  --layer equipements \
  --rules playground/scenario-3-rules.json \
  -o output/health_coverage.gpkg

gispulse serve output/health_coverage.gpkg

Expected result

Output schema
ColumnTypeDescription
access_ringfloatcost_budget value of the innermost ring that contains the building, or 99999 outside every ring
access_classintRing index 1-5 (1 = inside the 500 m zone, 5 = beyond 1.5 km)
access_colorstringHex from the RdYlGn-reversed palette (green → dark red)

Interactive playground

Live 3-step pipeline (requires the demo backend).

Step by step:

  1. filter_sante (orange) — keeps features where categorie == 'Santé' (hospitals, clinics, pharmacies, practices...).
  2. isochrone_rings (purple → blue gradient) — multi-source Dijkstra over routes in metric CRS EPSG:2154, one pass for the 4 budgets [500, 750, 1000, 1500]. Output: 4 stacked dissolved polygons, one per budget.
  3. classify_by_ring (multi-colour) — for every building, picks the smallest cost_budget among intersecting rings, maps it to a class index 1..5 (5 = outside every ring), and writes the palette colour.

Interactions:

  • Popup on health-facility markers (category, nature).
  • Isochrone polygons follow the real road network (not a circular buffer).
  • Click the map to add a new source: the isochrones are recomputed live.

Try it live

Live DemoList available capabilities (filter, isochrone, classify_by_ring, etc.).
GET/capabilities
Live DemoList datasets exposed by the demo, including clermont_ferrand_bdtopo.
GET/datasets

Going further

Published under AGPL-3.0 license.