A fast map + faceted search over ERA's Register of Infrastructure (RINF), for
every subclass of era:InfrastructureElement (operational points, sections of
line, tracks, sidings, tunnels, platforms, kilometric posts, ...), with PROD
and UAT side by side.
Demo: https://rinf-atlas.matdata.eu. It runs on a home server and is hosted on a best-effort basis, so expect occasional downtime.
Documentation: how datasets are updated, architecture, HTTP API, shareable links, design notes.
A line profile lays a national line, a series of sections of line picked on the map, or the route between two operational points out as a strip along the distance, with one row per parameter of their tracks (maximum speed, electrification, gauge, ETCS level, gauging, line category...; rows are configurable), linked to the map and downloadable as CSV.
Search and the map never query ERA live: a worker mirrors the RINF named graphs and indexes them into PostGIS. Only "describe" (and the graph traversal built on it) goes to ERA's SPARQL endpoint.
ERA dataset explorer API ──┐ ┌── web (nginx + MapLibre UI) :8080
ERA GraphDB (rinf-plus) ───┼─> worker ─> PostGIS ─┤
│ (Oxigraph mirror) └── api (FastAPI: facets, search, MVT tiles)
└──────────── live describe only ──────────┘
With the published images (amd64 and arm64, from GitHub Container Registry):
cp .env.example .env # optional: port, password, environments, graph subset
docker compose pull
docker compose up -d
docker compose logs -f workerOr build from source with docker compose up -d --build. Set ATLAS_VERSION
in .env to pin a release (for example 0.1.0) instead of latest.
Open http://<host>:8080. The first sync downloads every provider graph of
PROD and UAT (about 40 M and 27 M triples; the largest are DB InfraGO and
SNCF Réseau). Measured on a 4-core test box: all of PROD (61 graphs, 969k
features) took 34 minutes, then UAT follows; the map fills in as graphs land. To try it on a
few countries first, set for example ATLAS_ONLY_GRAPHS=0088,0084,0082
(Infrabel, ProRail, CFL) in .env.
Disk, measured for PROD: 7.6 GB mirror volume and 3.1 GB PostGIS; plan for about 15 GB + 6 GB with UAT too, plus 2 GB of temporary space while the largest graph downloads. RAM: 4 GB is comfortable.
Measured API timings on full PROD: unfiltered or repeated facet requests 50-200 ms, the first request for a new heavily filtered state 1-3.5 s, search about 80 ms, vector tiles 80-600 ms.
Users see what the backend has processed in the Datasets dialog (the "54/54 datasets processed" button top right): every dataset per environment with its infrastructure manager, country, file name, publication date, status (processed, processing, waiting, failed with the error, removed), feature and triple counts and when it was processed, plus when the worker last checked for new datasets. "Show on map" limits the map and facets to that dataset (shareable via the URL).
Every poll_interval_minutes (default 30) the worker, per environment:
- reads the dataset explorer API
(
{datasets_api}/dataset/published?latest=true&groupBy=im, the same call the explorer page makes) for the latest dataset per infrastructure manager; - lists the repository's named graphs (
/contexts); provider data lives inhttp://data.europa.eu/949/graph/{IM code}; - fingerprints each graph as dataset id | publication date | triple count
and only re-downloads graphs whose fingerprint changed (one streaming
N-Triples export per graph via the RDF4J
/statementsendpoint); - replaces the graph in the local mirror, re-extracts its features, and derives missing geometry; graphs that disappeared are removed;
- validates changed graphs against the SHACL shapes ERA publishes
(
graph/rinf/shacl). The Datasets dialog shows the violations and warnings per dataset, the map can ring features with issues, a feature's details list its results, and each report downloads as a standardsh:ValidationReportto explore in shacl-report. See docs/datasets.md.
shapes/*.ttl holds one SHACL node shape per class, plus shared.ttl for
facets every class has (country, infrastructure manager). They were generated
from the ERA ontology and the real data (properties used by fewer than 10
instances were left out) and are meant to be edited:
shape:operational-point a sh:NodeShape ;
atlas:key "operational-point" ;
sh:targetClass era:OperationalPoint ;
atlas:geometryPath [ sh:alternativePath ( ( gsp:hasGeometry gsp:asWKT )
( era:netReference gsp:hasGeometry gsp:asWKT ) ) ] ;
sh:property [
sh:path era:opType ; # any SHACL path: sequence, inverse, alternative, *, +, ?
atlas:facet "opType" ;
rdfs:label "Type of operational point" ;
atlas:kind "terms" ; # terms | range | boolean | text (search only) | link
sh:order 50 ;
] .All annotations are documented in shapes/atlas.ttl. The worker notices a
changed shape set on its next loop (within a minute) and re-indexes from the
local mirror; nothing is downloaded again (full PROD: about 20 minutes). Invalid shapes are logged and the
current index is kept.
Geometry comes from atlas:geometryPath; features without their own
geometry get one from linked features: atlas:lineBetween (era:opStart era:opEnd) for sections of line, atlas:geometryFrom (for tracks: the
operational point or section of line they are part of; platforms: their
track; and so on, resolved in chains).
Atlas layers load straight into a desktop GIS as OGC API – Features (one
layer per class, with every facet as an attribute; a class that mixes
geometry types, such as operational points with points and polygons, is one
layer per type: prod.operational-point.point, prod.operational-point.polygon,
and a feature with both a point and a polygon is in each layer with that part)
and as vector tiles. On the
demo:
| URL | |
|---|---|
| OGC API – Features | https://rinf-atlas.matdata.eu/api/ogc |
| One class as GeoJSON | https://rinf-atlas.matdata.eu/api/ogc/collections/prod.tunnel/items?limit=1000 |
| Vector tiles | https://rinf-atlas.matdata.eu/api/tiles/{z}/{x}/{y}.pbf?state=eyJlbnYiOiJwcm9kIn0 |
| Vector tile style | https://rinf-atlas.matdata.eu/api/style.json?env=prod (TileJSON: /api/tiles.json?env=prod) |
- Features: Layer › Add Layer › Add WFS / OGC API – Features Layer ›
New, paste the URL, Connect, pick the classes. QGIS fetches what is in view,
page by page. ArcGIS Pro: Insert › Connections › Server › New OGC API
Server. GDAL:
ogrinfo OAPIF:https://rinf-atlas.matdata.eu/api/ogc. - Filtered: in the app, choose classes and filters, open Results and
click Open in GIS; it copies an
/api/ogc/filtered/...URL that serves only what matches. Items also take the facets as query parameters, e.g..../prod.tunnel/items?lengthOfTunnel=1000..&country=http://publications.europa.eu/resource/authority/country/BEL. - Vector tiles: Layer › Add Layer › Add Vector Tile Layer › New Generic
Connection, the tiles URL as URL (max zoom 14) and the style URL as Style
URL for the map's class colours. Fast for a whole country, but tiles carry
only
id, iri, class, label(and below zoom 8 onlyid, class).
Details and limits: docs/api.md.
Run inside the worker container (docker compose exec worker atlas ...).
While the poll loop runs it owns the mirror, so these are queued and picked
up by the loop:
| Command | What it does |
|---|---|
atlas sync [--env prod] [--force] |
poll now; --force re-downloads everything |
atlas reindex [--env prod] |
re-extract from the mirror (also automatic on shape changes) |
atlas validate [--env prod] [--graph G] [--force] |
data quality: validate graphs against ERA's SHACL shapes (automatic after a sync) |
atlas shapes-tags [--env prod] |
list the ERA ontology tags quality.shapes_tag can pin the shapes to |
atlas generate-shapes [--force] [--no-data] |
regenerate starter shapes into shapes/ (keeps existing files unless --force) |
atlas check-shapes [-v] |
parse shapes/ and print the compiled facets |
atlas load-file --env prod --graph IRI file.nt |
index a local file (testing; stop the worker first) |
config/environments.yaml lists the environments (SPARQL repository,
dataset explorer API, vocabulary graphs) and the provider-graph pattern.
.env sets the port, database password, ATLAS_ENVS (subset of
environments) and ATLAS_ONLY_GRAPHS (subset of graphs). Behind a TLS
proxy, set ATLAS_PUBLIC_URL (e.g. https://rinf-atlas.example.org) so the
OGC API links use it. The quality: block of environments.yaml (and
ATLAS_QUALITY* in .env) tunes or turns off the data quality validation,
per environment if needed: which SHACL shapes (ERA's published graph, or a
pinned tag of ERA's ontology repository such as v3.3.4) and the largest
dataset to validate (15M triples by default).
To publish a privacy statement for your instance, put a page at
site/privacy.html (start from site/privacy.example.html); the app then
links it as Privacy. docs/privacy.md explains how and
lists what the app processes, as a basis for what to disclose.
To add your own map layers (aerial photos, a WMS, vector tiles, including
private ones that need an API key), list them in config/layers.yaml (start
from config/layers.example.yaml); they appear in the Map layers menu.
Keys go in .env and stay on the server: the API proxies those tiles.
docs/layers.md explains how and the trade-offs.
| Path | |
|---|---|
worker/ |
ingest worker (atlas CLI): discovery, Oxigraph mirror, SHACL-driven extraction, shape generator |
api/ |
FastAPI app: facets, search, extent, vector tiles (ST_AsMVT), feature, export, OGC API – Features, live describe |
web/ |
TypeScript + MapLibre + vis-network frontend (built into the nginx image) |
shapes/ |
facet shapes (edit these) |
site/ |
optional pages for your instance, such as privacy.html |
db/init.sql |
PostGIS schema |
docs/api.md |
HTTP API contract |
docs/urls.md |
shareable links: the URL scheme of the web app |
docs/datasets.md |
how datasets are updated |
docs/architecture.md |
architecture overview |
docs/DESIGN.md |
design notes |
docs/privacy.md |
adding a privacy statement; what the app processes |
docs/layers.md |
adding your own map layers, with API keys |
Bugs, feature requests and questions: open an issue. The web app links here from its About dialog (the ⓘ button at the top right).
See CONTRIBUTING.md: the frontend runs against a bundled
mock API, and tests/smoke.sh indexes a small Luxembourg sample
(tests/fixtures/) into a running stack and checks every endpoint, after which
the Playwright tests in web/e2e/ drive the UI in a browser at desktop and
phone sizes. CI runs lint, tests, the frontend build and both end-to-end
suites; pushes to main and version tags publish the images.
Code: MIT. The MIT licence covers this repository's code only, not the data the app shows or the services it loads at runtime:
- RINF data comes from the European Union Agency for Railways' Register of
Infrastructure (RINF). ERA publishes it because EU law requires it
(Directive (EU) 2016/797, Regulation (EU) 2019/777). The infrastructure
managers who submit it remain responsible for it, and ERA's
RINF terms of use
apply. No data is stored in this repository except the small test extracts in
tests/fixtures/, which credit their sources. This project is not affiliated with ERA. - ERA vocabulary: the facet shapes in
shapes/use class and property IRIs and labels from the ERA ontology (EUPL 1.2). - Basemap: raster tiles from the OpenStreetMap tile server (data © OpenStreetMap contributors, ODbL). The servers are donated, so follow the tile usage policy: a busy deployment should point at its own or a commercial tile server.
- Rail overlay (optional): OpenRailwayMap tiles (CC-BY-SA 2.0, data ODbL), free only for small, public, non-commercial use (conditions).
- ERA SHACL shapes (the data quality rules), © ERA, are licensed under the
EUPL 1.2, as the shapes and ERA's
ontology repository
state. The worker mirrors them from ERA's repository like the data, or, when
quality.shapes_tagis set, downloads them from that GitLab repository at the tag (same licence) into its cache; they are not part of the images.tests/fixtures/era-shacl.nt.gzis an unmodified copy for the tests, credited in tests/fixtures/README.md. - shacl-report: the worker image includes the
shacl-report 1.0.0 binary
(Apache-2.0, by Mathias Vanden Auweele) to render the reports; its
LICENSEandTHIRD_PARTY_NOTICES.md(the licences of the libraries compiled into it and embedded in its HTML reports, all permissive) ship with it in the image under/usr/share/licenses/shacl-report/. - Dependencies are under permissive licences (MIT, BSD, ISC, Apache-2.0) or weak copyleft used unmodified: psycopg (LGPL-3.0) and, at build time only, lightningcss (MPL-2.0). The PostGIS image (GPL-2.0) runs as an unmodified, separate container.
