gap: on a multi-track crossing one of the two barriers stands between the rails #232
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#232
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
The gap between a crossing's two installations comes from the register's
aantal_sporenalone:crates/geo/src/crossings.rs:377-382, fed fromCrossingPoint::tracks(crates/geo/src/crossings.rs:256-260) and consumed atcrates/geo/src/crossings.rs:391, where the two installations are placed at±clearancealong the road (crates/geo/src/crossings.rs:403).The Peizerweg AHOB registers
aantal_sporen: 1— pinned against the captured service answer atcrates/cartopolis/src/systems/map/crossings.rs:430-438— soclearance = SETBACK_M= 3.5 m and the pair stands 7 m apart. The issue's Overpass check finds three running lines within 6.8 m of that point, so the northern installation stands between two of them.Two facts already in the tree say the register's count is the wrong source rather than a slightly wrong one: 68 features register 0 tracks and the range reaches 14, both of which
clearance_mclamps away (docs/notes/prorail-level-crossings.md:143-146).The tile the crossing is meshed from already carries the answer.
place_crossingsbuilds aWayFieldwithWayFilter::ExcludingRail(crates/geo/src/crossings.rs:312) precisely because the register's point sits on the rail centreline; the samestreetslayer holds those rail features, andWayFilteris the seam that already decides which of them count (crates/geo/src/furniture.rs:349-368,crates/geo/src/road_texture.rs:167-169).I decoded the tree's own real-tile fixture read-only to check the shape of that data —
crates/geo/tests/fixtures/groningen_z14.mvt, which is z14/8490/5319 (crates/geo/src/vector_tiles.rs:4507-4511,crates/cartopolis/src/systems/map/tile_loader.rs:842), i.e. not the Peizerweg cell. Itsstreetslayer holds 95 features, of which 3 arekind = rail: parallel tracks arrive as separate features, and one carriesservice = crossover. So the layer does carry more than one track per corridor. Whether the Peizerweg cell's tile carries all three of the ways the issue lists is not established here — the note already records that measurement as never taken (docs/notes/prorail-level-crossings.md:235-237), and it is the first step under Verification.Approach
Measure how far the rails crossing this road span, in the same frame and off the same tile the yaw already comes from, and let the register's count be a floor rather than the source.
crates/geo/src/furniture.rsWayFiltervariant — rail only, the exact complement ofExcludingRailthroughroad_texture::is_rail_kind, so the two cannot drift (crates/geo/src/furniture.rs:363-368).half_widthanswersNonefor it (a rail is not a carriageway), as does the#[cfg(test)]push_linearm (crates/geo/src/furniture.rs:504-517).direction_and_width: given a point, a yaw and a radius, the greatest |offset along that yaw| at which an indexed segment crosses the line through the point in that direction, orNone. It reusesnearest's bucket window (crates/geo/src/furniture.rs:602) to pick candidates, then for each candidate takes the signed perpendicular offsets of its two endpoints against the road line, keeps only sign-changing segments (a rail running parallel to the road never crosses it and must not widen anything), interpolates the crossing point and projects it onto the yaw. Duplicates from multi-bucket segments are harmless — it is a maximum.crates/geo/src/crossings.rsplace_crossingsbuilds a second field from the sameTileDatawith the new filter. It is behind the existingpoints.is_empty()early return (crates/geo/src/crossings.rs:309), so a cell with no registered crossing pays nothing, and the furniture'sWayFilter::Allpath is untouched.place_crossings_againsttakes both fields and records the measured span onLevelCrossingas a newOption<f32>field, in metres, measured with the yaw it just resolved — the same "one lookup, one road" ruleroad_halfalready follows (crates/geo/src/crossings.rs:298-302).Nonewhen nothing crossed.push_crossingcombines the two sources instead of replacing one with the other:and the span query is asked for
ceilingas its radius, so the search reaches exactly as far as the result is allowed to.Why
max, not "the tile wins": both sources undercount and neither can be shown to overcount this geometry. The register undercounts here (1 against 3). OSM undercounts wherever a double track is mapped as one way, which would take a 2-track crossing back to 3.5 m — a regression the current code does not have.maxis also the rule the project already states for surveyed data over generated data (docs/notes/prorail-level-crossings.md:44-52, "add don't replace").Why the placement stays symmetric:
direction_and_widthreturns either of two opposite yaws and says so (crates/geo/src/furniture.rs:573-576, restated atcrates/geo/src/crossings.rs:268-272); anything built on it must be symmetric under a half turn. So the measured span is a singlemax |offset|applied to both sides, not a per-side offset — the Peizerweg's north/south asymmetry (5.9 m against 6.8 m) is deliberately collapsed to the larger.crates/cartopolis/src/systems/map/map_geometry.rs— no change expected:place_crossings(&tile, tile_m, &points)keeps its signature (crates/cartopolis/src/systems/map/map_geometry.rs:740), and it is the only client call site.Docs —
docs/notes/prorail-level-crossings.md: theaantal_sporencensus line "It sets how far apart the two installations stand and nothing else" (:143-146) stops being true, and the orientation section (:150-172) gains the other half of the same idea — the rail is excluded from the direction field and is now the source of the spacing. The module note atcrates/geo/src/crossings.rs:284-302needs the same.Acceptance criteria
WayFilterhas a rail-only variant defined as the complement ofExcludingRailviaroad_texture::is_rail_kind, with no second list of kinds.kindtag, on the pattern ofthe_tile_side_filter_reads_the_kind_tag(crates/geo/src/crossings.rs:702-729): a synthstreetslayer whose only feature iskind = railis found by the rail-only field and not byExcludingRail.tracks: 1. Every meshed vertex of both installations lies at least 6.8 m from the crossing point along the road — i.e. no mast or boom stands inside the outermost rails — read back offbuild_crossing_meshesoutput, not recomputed, the wayacrossdoes it (crates/geo/src/crossings.rs:518-543).an_unknown_width_falls_back_to_the_old_numbersstates (crates/geo/src/crossings.rs:990-1010).tracks: 4keepsclearance_m(4).clearance_m(MAX_TRACKS as u8), sothe_track_count_is_bounded_at_both_ends(crates/geo/src/crossings.rs:863-869) keeps its meaning.yawand atyaw + πproduces the same bounding box, pinning that the unresolved yaw sign is still safe.docs/notes/prorail-level-crossings.mdand thecrossings.rsmodule note state that the spacing is measured from the tile's rail with the register as a floor, and no longer sayaantal_sporensets it alone.crates/geo, and no change toMAX_BOXES_PER_CROSSING/ the byte accounting — this moves geometry, it does not add any.Verification
Do this first, before writing the fix. Fetch the Peizerweg cell's tile and count the
kind = railfeatures near the register point:then decode it (a
#[test]over the bytes, or the throwaway script style used to readgroningen_z14.mvtabove). The register point is 53.210238, 6.549328. If the tile carries only the one running line, the measured span at Peizerweg is 0, the register floor governs, and the reported capture will not change — say so on the issue rather than shipping a change that cannot be seen. If it carries the neighbours, add the tile ascrates/geo/tests/fixtures/peizerweg_z14.mvt(LFS-tracked by extension, like the existing.mvtfixtures) and pin the real span in a test.Suite:
The picture:
level_crossingsmust stay 3 (crates/cartopolis/src/systems/dev/shot_harness.rs:651) — this change must not drop an installation. The northern barrier standing clear of the second track is a geometry and placement judgement, which is trustworthy on the software rasteriser (docs/notes/headless-shots-software-renderer.md); this refinement pass is read-only and cannot run it, so the implementing agent owns that capture.Out of scope
±clearanceand stay symmetric, because the yaw sign is unresolved by construction. Placing each mast at its own side's rail would first needdirection_and_widthto answer which way the road points.maxcannot narrow that, and nothing here makes it worse.service. Aservice = crossoveror yard track that crosses the road counts like any other — the issue's own listing counts the yard track as one the barrier fails to guard.--dump-statefield for the clearance. There is no metric for it today and none is added; the gate stays the unit tests plus the capture.docs/notes/prorail-level-crossings.md:218-228).spoorastrack axes — rejected with reasons atdocs/notes/prorail-level-crossings.md:40-66, and this change is the argument for not revisiting them: the rail geometry needed is already in the tile.Open questions
None.
Branch:
fix/232-crossing-clearance-from-railsOriginal request
What I did
The same capture as the barrier-orientation issue — the Peizerweg AHOB in Groningen,
--at 53.210238,6.549328,25 --look=-90,0, settled,level_crossings = 3— and thenmeasured the actual track layout at that point against OSM.
What happened
The spacing between a crossing's two installations comes from the register's
aantal_sporenand from nothing else:The Peizerweg feature reports
aantal_sporen: 1— verified against the live servicetoday, and pinned by
the_real_answer_reads_as_one_half_barrier. So clearance is 3.5 mand the two installations stand 7 m apart, straddling one track.
The road crosses more than one. Overpass, around the register's own point:
plus four separate
railway=level_crossingnodes, every one of themcrossing:barrier=half.3.5 m north of a point on the first rail is 2.4 m short of the second. In the 25 m
top-down capture the northern barrier lies squarely between the two ballast ribbons,
with its mast standing in the second track's ballast — a barrier inside the crossing it
is meant to guard, and the second track completely unguarded.
What a user would expect
A barrier stands clear of the outermost rail it bars. Nobody expects one planted between
two running lines.
Where the seam is
crates/geo/src/crossings.rs::clearance_m, fed fromCrossingPoint::tracks.place_crossingsalready builds aWayFieldthat knows which ways are track — it isbuilt with
WayFilter::ExcludingRailprecisely to keep the rail out of the orientation —so the same
TileDatacan answer "how far do the rail ways crossing this road span?"directly, with the register's count as a fallback rather than the source. The layer's own
note gives two more reasons not to trust the field: 68 features register 0 tracks and
the range reaches 14, both of which
clearance_malready has to clamp away.Filed from the QA pass on #216 (#230).
🤖 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.
🤖 Promoted into the build lane by the 7-day retrospective (#279) —
autonomous+ship.Why this one.
railway=railways at 0.0 m, 5.9 m and 6.8 m from the register's own point, againstaantal_sporen: 1and the 3.5 m clearance that follows from it. Whether a barrierstands clear of the outermost rail is a vertex position — value 6, and geometry is
evidence on this hardware.
place_crossingsalready builds aWayFieldwithWayFilter::ExcludingRail, so the rail span is in hand at the pointclearance_miscalled. The register's count becomes the fallback rather than the source, which is what
the layer's own note already recommends (68 features register 0 tracks; the range
reaches 14, and both are clamped away today).
retrospective, and both landed on 2 September with tests built on the committed
fixtures. This is the third and last of the geometry findings from that QA pass.
a measurement already loaded in the same function.
Bounds.
crates/geo/src/crossings.rs. Keepaantal_sporenas the fallback so a cellwhose tile carries no rail geometry still places an installation — value 3, degrade,
never break. Do not touch the register parse,
MAX_TRACKS, or the boom work that justlanded.
🤖 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 — 24 turns · $2.93 · session
ff31c662-bbd4-4b9a-975b-2a66aeb8fcfc. Reply with @agent to continue this session.🤖 Picked this up — working on branch
fix/232-crossing-clearance-from-railsin 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.A crossing's two barriers now stand outside the tracks the road actually crosses, instead of at a distance the register guesses.
What changed
crates/geo/src/furniture.rs: added a third way-filter,RailOnly, defined as the negation of the existingExcludingRailarm, so there is one list of rail kinds and not two. AddedWayField::crossing_span, which takes the crossing point, the road direction and a radius and answers how far along the road a rail crosses it. It only counts a segment whose two ends fall on opposite sides of the road line, so a track running parallel to the road widens nothing. The bucket-window walk thatnearestalready did is now a shared helper;nearestbehaves exactly as before.crates/geo/src/crossings.rs:place_crossingsbuilds a second field over the same tile with that filter and records the measured span on each crossing.aantal_sporenis now the floor, not the source: the spacing is the larger of the register's number and the measured span plus the 3.5 m setback, clamped to the ceiling the 16-track cap already allowed (37.25 m). The larger wins because both sources undercount — the register here, and OpenStreetMap wherever a double track is drawn as one way. The span is one distance applied to both sides, because the road direction's sign is unresolved and everything built on it has to survive a half turn.docs/notes/prorail-level-crossings.md: the line sayingaantal_sporensets the spacing is corrected, and a new section records the measurement, themaxrule and what stays out of scope.The measurement the issue asked for first
I fetched the Peizerweg cell (zoom 14, tile 8490/5321) and decoded it. It carries three railway features; two of them cross the road, at 0.52 m and 5.89 m from the register's own point. So the register saying one track really does put the northern installation between two running lines. The tile is now a test fixture (
crates/geo/tests/fixtures/peizerweg_z14.mvt, stored through Git LFS like the other tile fixtures) and a test pins the 5.89 m span through the real code path.Verified
cargo test -p cartopolis_geo— 247 passed, including 8 new tests: the filter is the complement of the road one on a real tag; no meshed vertex of either installation lies inside the outermost rail in the reported layout; a parallel rail moves nothing; with no rail in the tile every form is placed exactly where it was; the register still wins when it claims more tracks; the ceiling holds both inside and outside the search radius; and the same crossing meshed at the two opposite road directions gives the same bounding box.cargo test -p cartopolis— 830 passed; the crossing tests that pin the captured service answer are unchanged.cargo fmt --checkover the five owned crates: clean.level_crossingsis still 3, and measuring the picture: the two ballast ribbons sit 102 pixels apart (5.89 m), and the two red-and-white booms sit clear above and below both of them. At the old 3.5 m spacing the northern one would land in the 43-pixel gap between the ribbons, which is the reported bug.Not verified
Both captures report
settled: falsewith map tiles still streaming — that is this machine's software renderer being slow, not something this change caused, and the crossings and rails were fully drawn. Colour and lighting are never evidence here, so how the installation reads at walking distance still wants a look on a workstation or the phone.For you to decide
Nothing blocking. Two things left deliberately alone, both noted in the file: a crossing that registers 14 tracks where the road crosses two still spreads its masts 37 m apart, since taking the larger cannot narrow it; and a yard track or crossover that crosses the road counts like any running line.
Branch
fix/232-crossing-clearance-from-railsis pushed.Branch
fix/232-crossing-clearance-from-rails(pushed)🤖 viberfox issue agent on the VPS — 78 turns · $9.26 · session
5d628352-db0d-46fa-9b50-36a8b2f6fec2. Reply with @agent to continue this session.🤖 Marked
agent:donewithout a CI verdict on10bcb20f— no workflow reported on this commit. The branch is waiting for review; check the run yourself before landing it.🤖 Not shipping this one — the branch conflicts with
main(another ticket landed first).Rebase
fix/232-crossing-clearance-from-railsontomain, resolve, push, and put theshiplabel back to land it.