ProRail Spoorwegen, PDOK OGC API: is it worth wiring in? #216
Labels
No labels
agent
agent:ci
agent:done
agent:failed
agent:needs-input
agent:refined
agent:refining
agent:running
agent:shipped
agent:skip
autonomous
autopilot
driven
local
plan
proposal
qa
qa-gap
research
retro
ship
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jeroen/cartopolis#216
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
docs/direction.md:86carries ProRail Spoorwegen as an unverified candidate. The first half of the ticket — interface, licence, volume, and whether it changes the picture — is answered below from live read-only probes on 2026-08-30. The answer is partly worth it: one of the seven collections earns a place, six do not.The interface.
https://api.pdok.nl/prorail/spoorwegen/ogc/v1— keyless OGC API Features,bboxquery, GeoJSON out (CRS84 by default, longitude first), cursor-paged withlimitclamped to 1000 (limit=5000answers 200 and returns 1000).Access-Control-Allow-Origin: *andCache-Control: public, max-age=3600, so the web build reaches it directly.properties=subsetting is a 400 — the same shapesystems::nav_marksfound on the RWS service (crates/cartopolis/src/systems/map/nav_marks.rs:1-48).numberMatchedis never returned.The licence: CC0 1.0, stated by the service's own landing page
rel="license"link. No obstacle to caching or redistributing an extract, and no credential needed.Seven collections, measured over four z14 cells (the zoom
map_geometrystreams geometry at):spooras(track axes)wissel(switches)overweg(level crossings)kilometrering(km posts)kruising(diamond crossings)stationtrace(corridor centreline)What is drawn today, and what is not. A railway reaches the client as a Shortbread
streetsfeature withkind = rail, drawn as a 3.2 m ballast ribbon with the rails painted on —crates/geo/src/road_texture.rs:149classifies it,:198gives the width,:311–:336paint sleepers then rails into the texture. Nothing three-dimensional exists anywhere on a railway:FurnitureKind(crates/geo/src/furniture.rs:110-160) has no barrier, mast, cross or signal,FURNITURE_TABLE(:228-250) names norailwaykey at all, and the module's own rule is that "anything unlisted gets nothing" (:22-24). A level crossing is currently a street ribbon meeting a rail ribbon with no equipment on it; the BGT supplies only the surface under it (crates/geo/src/paving.rs:16), and arailwayPOI becomes a label and a proximity prompt, not geometry (crates/geo/src/places.rs:152).So
overwegis the collection that changes the picture, and the other six do not:spooras/trace— track centrelines. Drawing them would lay a second set of rail ribbons on top of the OSM ones already drawn atroad_texture.rs:198, which is the coplanar z-fight this codebase keeps paying for; substituting them for the OSM rail is the argumentpaving.rs:7-35already lost for road surfaces (the ProRail axis carries no texture, no casing and no rung on the depth ladder), and it would delete every railway outside the Netherlands. Value 4.wissel/kruising— switches and diamonds, with real geometry and real attributes (hoekverhouding1:9,max_snelheid_afbuigend,kromme_stand). There is nothing to draw them on: the rails are a painted texture, not modelled steel, so a switch is invisible at every altitude. Value 6 — no--dump-statefield would move for anything a person could see. This becomes worth revisiting only if rails are ever modelled as geometry, which is a different ticket.station— already a POI throughplaces.rs:152.kilometrering— a small unlabelled plate on a post,rotatienull on all 6,000 features sampled, so it has no orientation either.furniture.rs:104-108: "a kind that would need a texture, a curve or a survey to read correctly is better left undrawn." An unlabelled km post is a stick.The
overwegregister, censused nationally 2026-08-30 — 3,917 features, 6.1 MB raw, 4 pages:overweg_type:Geen2,035 ·AHOB1,157 ·AHOB-MINI381 ·AOB137 ·ALI63 ·Onbekend40 ·AKI24 ·HALI21 ·WILO16 ·VKL9 ·HAVIO9 ·HBKI8, and a tail. 100% filled.karakter:Openbare overweg2,022 ·Onbekend587 ·Dienstoverpad557 · private 263 · public-in-character 214 · footpath 158 · … 100% filled.aantal_sporen(tracks crossed): 1 → 2,091 · 2 → 1,269 · 3 → 232 · 4 → 126 · 0 → 68 · up to 14. 100% filled.vri(road signals):Ja24.type_hekandaantal_andreas: null on all 3,917 — the register does not record the St Andrew's cross.azimut: null on all 3,917. The orientation has to be derived.levenscyclus_status:Bestaand2,612,Definitief ontwerp1,304. This is not a built/not-built flag — the Peizerweg crossing in Groningen readsDefinitief ontwerpand exists, as do every sampledspoorasandwisselfeature. Filtering on it would delete a third of the register. This is the same trapdocs/notes/surveyed-street-objects.mdrecords for the BGT's retired trees, in the opposite direction.aantal_ahobis 100% filled but not usable as a physical boom count: the single-track Peizerweg crossing reports 15.Approach
Mirror
systems::nav_marksexactly — it is the in-tree precedent for a keyless CORS-open PDOK collection fetched by the client itself rather than through cartopy, and its module note (crates/cartopolis/src/systems/map/nav_marks.rs:1-48) states the reasoning that applies here unchanged: no extractor, noCoverageSlot(coverage is an envelope), consumed insidemap_geometry's existing per-cell worker because the objects end up merged into that cell's furniture mesh.crates/geo/src/crossings.rs(new, no Bevy) — the pure half:LevelCrossing { pos: [f32; 2], yaw: f32, kind: CrossingKind, tracks: u8 }in tile-local metres, the frame the rest offurniture.rsworks in.CROSSING_TABLEmappingoverweg_typecodes to forms, followingFURNITURE_TABLE's rule atfurniture.rs:22-24: a code the table does not name draws nothing, andGeen/Onbekendare explicitly nothing. The half-barrier codes are the ones with a form; the flashing-light-only codes get a light post or nothing, and the table records which.build_crossing_meshes(&[LevelCrossing], max_bytes: Option<usize>) -> Vec<SurfaceMesh>, byte-bounded exactly asbuild_mark_meshesandbuild_line_meshesare, emitting into the sameSurfaceGroupthe furniture uses so it shares that group's material, altitude fade (FURNITURE_FADE_START/_END,furniture.rs:59-62) and toggle.MAX_CROSSINGS_PER_CELL, in the shape ofMAX_FURNITURE_PER_CELL(furniture.rs:69) andMAX_MARKS_PER_CELL.WayField(furniture.rs:345-420) already answers "which way does the nearest way run", but it indexes "every street, path and railway in the tile" (:345) and reads nokindtag (:371-412) — and anoverwegpoint sits on the rail centreline by the collection's own description, so the nearest way is always the track. This needs a rail-excluding variant of the field (or an equivalent), and it is the one genuinely new piece of logic in the ticket.crates/cartopolis/src/systems/map/crossings.rs(new) — the streaming half, mirroringnav_marks.rs:52-90and:408-470:CROSSING_ZOOM = 14matchingGEOMETRY_ZOOM,https://api.pdok.nl+/prorail/spoorwegen/ogc/v1,PAGE_LIMIT = 1000, the collection's ownextent.spatial.bboxas the envelope gate, a compacted per-cell cache instorage::Namespace::Cache(the raw feature is ~2 KB of mostly administrative properties), and the per-cell cap applied on load.Gates before a request is made, three, matching
map_geometry.rs:464-478:wants.furniture— crossings ride the furniture group's mesh, material and toggle (data_layers.rs:491-499), so a run with it off asks for nothing;vector_tiles::body_mentions_water(crates/geo/src/vector_tiles.rs:3949-3984) is the pattern, and it transfers with one difference worth stating: water is a layer name, rail is akindvalue insidestreets, and MVT stores string values verbatim in the layer's value table — so acontains_bytes(body, b"rail")test is conservative in the same direction (a tile with a rail feature cannot fail to contain it; a street named "…rail…" is a harmless false positive). Add it besidebody_mentions_waterwith that asymmetry written down.Wiring:
map_geometry.rs:464-478— fetch the cell's crossings on the I/O pool besideload_marks_cell, behind the three gates.map_geometry.rs:693-704— build the meshes besidebuild_mark_meshes, inside the same worker, under the samemesh_bytescap. No collider, for the reason stated there for marks.world_places.rs:54,:283,:377-384— thesurveyedcounter tuple grows from six to seven.shot_harness.rs:633-640,:835(CSV header),:2653,:2768,:3221— alevel_crossingsfield besidenav_marks, so--expectcan gate it.LICENSE-THIRD-PARTY.md— a record in the documented format (settings.rs:1342-1350, andLICENSE-THIRD-PARTY.md:85-91for the AHN record's shape). CC0 compels no attribution, but AHN is CC0 and is listed, so this follows the file's own convention.data_layers.rs:2503-2533is where the Layers panel's source line goes.docs/notes/prorail-level-crossings.md— new note withtopic:/triggers:/updated:front matter perdocs/notes/README.md: the endpoint and its 404-adjacent path, the licence link, the per-cell and national volume tables above, the fulloverweg_type/karaktercensus, and the three findings that will otherwise be rediscovered —azimutnull everywhere,levenscyclus_statusis not a built flag,aantal_ahobis not a boom count.docs/direction.md:86— move the ProRail row from Candidates to Wired, naming what was taken and pointing at the note for the six collections that were not.Acceptance criteria
crates/geostill has no Bevy dependency; the crossing type, the code table and the mesh builder are all incrates/geo/src/crossings.rs.overweg_typethe table does not name places nothing — asserted by a test, mirroringfurniture.rs:22-24.GeenandOnbekendplace nothing.levenscyclus_status; a test or a comment records why (the Peizerweg crossing readsDefinitief ontwerp).aantal_ahobas a boom count, for the reason recorded in the note.build_crossing_mesheshonoursmax_bytesand splits or truncates, exactly asbuild_mark_meshesdoes; nothing is uploaded outsidemap_geometry's existing per-cell worker, build cap and despawn cap.nav_marks.rs:434-440.furniture_enabledfalse, or on a tile body with no rail mention, no HTTP request is made.kind = railstreet; the asymmetry is stated in its doc comment asbody_mentions_water's is.level_crossingsappears in theshot statelog line, the--csvheader and--dump-state, and is assertable with--expect.LICENSE-THIRD-PARTY.mdgains a ProRail record andparse_noticesstill parses (the_notices_format_parses).docs/notes/prorail-level-crossings.mdexists with the measurements and the three traps;docs/direction.md's ProRail row moves to Wired and says which six collections were rejected and why.Verification
Scene gate — the Peizerweg AHOB crossing,
6.54933, 53.21024, in the z14 cell8490/5321measured above:and a control over a cell with no railway (Grote Markt,
53.2194,6.5665) assertinglevel_crossings==0, which is also what proves the rail gate suppresses the request rather than the parse.Only elsewhere. This container renders headlessly on lavapipe, so geometry, placement and counts from the two commands above are trustworthy; whether the installation reads as a level crossing at walking distance is colour and lighting, and
docs/notes/headless-shots-software-renderer.mdsays that is not evidence here. Judge the picture on a workstation or through the Android emulator.Deferred measurements (not taken in this pass, none of them blocking):
--csvwith--framesacross a rail corridor.kilometreringtotal; paging stopped at 6,000 in 6 pages.streetsfeatures withkind = raila Shortbread z14 tile carries over Groningen station. Only needed if somebody later re-proposesspooras; it does not change this ticket, which does not draw track geometry.Out of scope
road_texture.rs:198,:311-336) stay exactly as they are.FurnitureInstances and reach nothing incollision::sync_prop_colliders, matching the marks.nav_marks,heightandaerial; nothing is added toserver/pipeline/.Open questions
None. The licence is CC0 1.0 stated by the service, the API is keyless, and the scope questions are settled by
docs/direction.md's values 4 and 6 as argued above.Branch:
feat/216-prorail-level-crossingsOriginal request
From the frontier in
docs/direction.md: ProRail Spoorwegen, PDOK OGC API — Track axes, switches, level crossings and kilometre posts for the whole rail network, none of which the OSM rail ribbons this client already draws carry: 22 track axes and 70 switches, 208 KB, over the Groningen station cell, against one axis and no switches a cell to the north..The first half of this ticket is deciding whether it is worth doing at all:
Not worth it is a valid answer. Record it in
docs/direction.mdagainst this rowwith the reason, and close this ticket — that is the result, not a failure.
Decide it yourself. This ticket is not being watched, so a question asked
here is a ticket that stops. The seven values at the top of
docs/direction.mdexist to settle exactly this kind of ambiguity — pick the reading they support,
say in the commit body which one you applied and why, and build. Only a decision
that would need something nobody can derive from the repository — a credential, a
licence somebody must agree to, a choice about what the project is for — is a
reason to stop.
If it is worth doing, follow the shape the surveyed layers already use:
an extractor under
tools/, a coverage manifest, a streamer that adds to whatis drawn rather than replacing it, and no unbounded per-frame upload.
Filed by the autopilot.
🤖 Refined by the viberfox issue agent. Reply with @agent refine and what is wrong to have this rewritten.
🤖 Refining this before anything gets built — reading the code, no branch, no commits. If it turns out I'd have to guess at the requirements I'll stop and ask instead.
🤖 Refined. The description above is now the spec; I'll pick it up for implementation on the next poll.
If the direction is wrong, say so now — @agent refine with a correction rewrites it, and
agent:skipstops me.🤖 viberfox issue agent on the VPS — 41 turns · $3.97 · session
7065d027-fa4a-497c-bd34-98bd28b8ac89. Reply with @agent to continue this session.🤖 Picked this up — working on branch
feat/216-prorail-level-crossingsin a fresh worktree on the VPS. I'll report back here when I'm done.Label an issue
agent:skipto keep me off it, ordrivento keep it in a session you are driving.Level crossings are now drawn where ProRail registers them — masts, lamp heads and red-and-white booms standing on the road either side of the track. Before this, a level crossing was a road ribbon meeting a rail ribbon with no equipment on it at all.
What I took, and what I did not. The service publishes seven collections. Only
overweg(level crossings) is wired in. Track axes and the corridor centreline would lay a second set of rail ribbons on top of the ones already drawn — value 4, add don't replace. Switches and diamond crossings have real geometry and nothing to stand on, because the rails are a painted texture rather than modelled steel, so they would be invisible at every altitude — value 6. Stations are already points of interest, and kilometre posts are unlabelled plates with no recorded orientation.The one hard part. The register carries no orientation (
azimutis empty on all 3,917 crossings) and puts each crossing's point on the rail centreline. The existing "which way does the nearest way run" index includes railways, so it answered the track every time and would have laid every boom along the rails it is meant to bar. I added a filter to that index that excludes rail, with the rail test sitting next to the road classifier so the two cannot drift apart. A crossing with no road in range is dropped rather than drawn at a guess.Three things in the register that are not what they look like — all measured against the live service and pinned by a captured feature in the tests:
levenscyclus_statusis not a built/not-built flag. 1,304 of 3,917 crossings read "Definitief ontwerp", including the Peizerweg crossing in Groningen, which is standing in the street. Filtering on it would delete a third of the register.aantal_ahobis not a count of booms. That same single-track crossing reports 15.How it is bounded. Three checks run before any network request: the coverage envelope, a railway mention in the not-yet-decoded map tile, and the existing Furniture layer toggle. There is a per-cell cap that warns when it truncates, and the meshes are byte-bounded inside the map streamer's existing worker. No colliders and no animation.
Verified here:
cargo fmt --checkclean across the workspace;cargo checkclean on both crates; the map-data crate's 236 tests and the client's 814 tests all pass, including 20 new ones. A correction along the way: I first asserted the coverage check refuses Brussels. It does not, and cannot — a rectangle drawn round the Dutch corners contains Brussels and Cologne. The test and the note now say so; the cost is one empty request for a border cell that has a railway in it.Not verified, and it needs you. I could not look at it. This container renders on a software rasteriser, where geometry and counts are trustworthy but colour and lighting are not. Whether the installation actually reads as a level crossing at walking distance is exactly that kind of judgement. The scene gate is:
One thing to decide. I named 11 of the 18 installation codes. Five more (
WILO,VKL,HAVIO,EBO,ADOB— 39 crossings between them) draw nothing, because I could not establish what the abbreviations expand to and a guessed form is an invented survey. If you know what they are, adding them is one line each inCROSSING_TABLE.Branch
feat/216-prorail-level-crossingsis pushed. Note atdocs/notes/prorail-level-crossings.md; the frontier list indocs/direction.mdmoves ProRail from candidates to wired.Branch
feat/216-prorail-level-crossings(pushed)🤖 viberfox issue agent on the VPS — 133 turns · $17.06 · session
ce25c591-c467-4ffd-919b-7a1a1dbea273. Reply with @agent to continue this session.🤖 Marked
agent:donewithout a CI verdict ondb60aea6— no workflow reported on this commit. The branch is waiting for review; check the run yourself before landing it.🤖 Merged into
mainas pull request #227 (db60aea6).🤖 Deployed to the public services.
railis a Shortbread attribute key #236