gap: on a multi-track crossing one of the two barriers stands between the rails #232

Closed
opened 2026-08-30 10:27:45 +00:00 by viberfox-agent · 7 comments
Collaborator

Problem

The gap between a crossing's two installations comes from the register's aantal_sporen alone:

fn clearance_m(tracks: u8) -> f32 {
    let tracks = u32::from(tracks).clamp(1, MAX_TRACKS) as f32;
    (tracks - 1.0) * TRACK_SPACING_M * 0.5 + SETBACK_M
}

crates/geo/src/crossings.rs:377-382, fed from CrossingPoint::tracks (crates/geo/src/crossings.rs:256-260) and consumed at crates/geo/src/crossings.rs:391, where the two installations are placed at ±clearance along the road (crates/geo/src/crossings.rs:403).

The Peizerweg AHOB registers aantal_sporen: 1 — pinned against the captured service answer at crates/cartopolis/src/systems/map/crossings.rs:430-438 — so clearance = 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_m clamps away (docs/notes/prorail-level-crossings.md:143-146).

The tile the crossing is meshed from already carries the answer. place_crossings builds a WayField with WayFilter::ExcludingRail (crates/geo/src/crossings.rs:312) precisely because the register's point sits on the rail centreline; the same streets layer holds those rail features, and WayFilter is 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. Its streets layer holds 95 features, of which 3 are kind = rail: parallel tracks arrive as separate features, and one carries service = 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.rs

  • Add a third WayFilter variant — rail only, the exact complement of ExcludingRail through road_texture::is_rail_kind, so the two cannot drift (crates/geo/src/furniture.rs:363-368). half_width answers None for it (a rail is not a carriageway), as does the #[cfg(test)] push_line arm (crates/geo/src/furniture.rs:504-517).
  • Add one query beside 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, or None. It reuses nearest'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.rs

  • place_crossings builds a second field from the same TileData with the new filter. It is behind the existing points.is_empty() early return (crates/geo/src/crossings.rs:309), so a cell with no registered crossing pays nothing, and the furniture's WayFilter::All path is untouched.

  • place_crossings_against takes both fields and records the measured span on LevelCrossing as a new Option<f32> field, in metres, measured with the yaw it just resolved — the same "one lookup, one road" rule road_half already follows (crates/geo/src/crossings.rs:298-302). None when nothing crossed.

  • push_crossing combines the two sources instead of replacing one with the other:

    let ceiling  = clearance_m(MAX_TRACKS as u8);              // 37.25 m, today's own cap
    let measured = span.map_or(0.0, |s| s + SETBACK_M);
    let clearance = clearance_m(crossing.tracks).max(measured).min(ceiling);
    

    and the span query is asked for ceiling as 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. max is 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_width returns either of two opposite yaws and says so (crates/geo/src/furniture.rs:573-576, restated at crates/geo/src/crossings.rs:268-272); anything built on it must be symmetric under a half turn. So the measured span is a single max |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: the aantal_sporen census 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 at crates/geo/src/crossings.rs:284-302 needs the same.

Acceptance criteria

  • WayFilter has a rail-only variant defined as the complement of ExcludingRail via road_texture::is_rail_kind, with no second list of kinds.
  • A tile-side test proves the new filter reads the kind tag, on the pattern of the_tile_side_filter_reads_the_kind_tag (crates/geo/src/crossings.rs:702-729): a synth streets layer whose only feature is kind = rail is found by the rail-only field and not by ExcludingRail.
  • The reported case, synthesised: one N–S road, three E–W rails at 0, +5.9 m and −6.8 m along it, 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 off build_crossing_meshes output, not recomputed, the way across does it (crates/geo/src/crossings.rs:518-543).
  • A rail running parallel to the road within the search radius does not move either installation.
  • With no rail in the field, every form is placed exactly where it is today — the compatibility half, on the terms an_unknown_width_falls_back_to_the_old_numbers states (crates/geo/src/crossings.rs:990-1010).
  • The register still wins when it is wider: a single rail line in the field with tracks: 4 keeps clearance_m(4).
  • The ceiling holds: rails 200 m apart on either side cannot place an installation further out than clearance_m(MAX_TRACKS as u8), so the_track_count_is_bounded_at_both_ends (crates/geo/src/crossings.rs:863-869) keeps its meaning.
  • The same crossing meshed at yaw and at yaw + π produces the same bounding box, pinning that the unresolved yaw sign is still safe.
  • docs/notes/prorail-level-crossings.md and the crossings.rs module note state that the spacing is measured from the tile's rail with the register as a floor, and no longer say aantal_sporen sets it alone.
  • No new Bevy dependency in crates/geo, and no change to MAX_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 = rail features near the register point:

curl -s https://tiles.cartopolis.org/tiles/osm/14/8490/5321 -o /tmp/peizerweg_z14.mvt

then decode it (a #[test] over the bytes, or the throwaway script style used to read groningen_z14.mvt above). 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 as crates/geo/tests/fixtures/peizerweg_z14.mvt (LFS-tracked by extension, like the existing .mvt fixtures) and pin the real span in a test.

Suite:

cargo test -p cartopolis_geo crossings
cargo test -p cartopolis crossings
cargo fmt -p cartopolis -p cartopolis_geo -p cartopolis_core -p cartopolis_simulator -p cartopolis_android --check

The picture:

cargo run -p cartopolis -- --shot /tmp/peizerweg.png \
    --at 53.210238,6.549328,25 --look=-90,0 --dump-state /tmp/peizerweg.json

level_crossings must 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

  • Asymmetric installations. The two stand at ±clearance and stay symmetric, because the yaw sign is unresolved by construction. Placing each mast at its own side's rail would first need direction_and_width to answer which way the road points.
  • The over-count direction. A crossing registering 14 tracks where the road crosses two still spreads its masts 37 m apart. max cannot narrow that, and nothing here makes it worse.
  • Filtering rail by service. A service = crossover or 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.
  • A --dump-state field for the clearance. There is no metric for it today and none is added; the gate stays the unit tests plus the capture.
  • The St Andrew's cross, animation, colliders, a Layers toggle of its own — all four are standing "not drawn, and why" entries (docs/notes/prorail-level-crossings.md:218-228).
  • Anything about the register's other six collections, including spooras track axes — rejected with reasons at docs/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-rails

Original 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 then
measured 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_sporen and from nothing else:

fn clearance_m(tracks: u8) -> f32 {
    let tracks = u32::from(tracks).clamp(1, MAX_TRACKS) as f32;
    (tracks - 1.0) * TRACK_SPACING_M * 0.5 + SETBACK_M
}

The Peizerweg feature reports aantal_sporen: 1 — verified against the live service
today, and pinned by the_real_answer_reads_as_one_half_barrier. So clearance is 3.5 m
and the two installations stand 7 m apart, straddling one track.

The road crosses more than one. Overpass, around the register's own point:

way 399546810  railway=rail            0.0 m  (the register point sits on it)
way 550964277  railway=rail            5.9 m north
way 636652195  railway=rail (yard)     6.8 m south

plus four separate railway=level_crossing nodes, every one of them
crossing: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 from CrossingPoint::tracks.

place_crossings already builds a WayField that knows which ways are track — it is
built with WayFilter::ExcludingRail precisely to keep the rail out of the orientation —
so the same TileData can 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_m already 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.

## Problem The gap between a crossing's two installations comes from the register's `aantal_sporen` alone: ```rust fn clearance_m(tracks: u8) -> f32 { let tracks = u32::from(tracks).clamp(1, MAX_TRACKS) as f32; (tracks - 1.0) * TRACK_SPACING_M * 0.5 + SETBACK_M } ``` `crates/geo/src/crossings.rs:377-382`, fed from `CrossingPoint::tracks` (`crates/geo/src/crossings.rs:256-260`) and consumed at `crates/geo/src/crossings.rs:391`, where the two installations are placed at `±clearance` along the road (`crates/geo/src/crossings.rs:403`). The Peizerweg AHOB registers `aantal_sporen: 1` — pinned against the captured service answer at `crates/cartopolis/src/systems/map/crossings.rs:430-438` — so `clearance = 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_m` clamps away (`docs/notes/prorail-level-crossings.md:143-146`). The tile the crossing is meshed from already carries the answer. `place_crossings` builds a `WayField` with `WayFilter::ExcludingRail` (`crates/geo/src/crossings.rs:312`) precisely because the register's point sits on the rail centreline; the same `streets` layer holds those rail features, and `WayFilter` is 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. Its `streets` layer holds 95 features, of which 3 are `kind = rail`: parallel tracks arrive as separate features, and one carries `service = 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.rs`** - Add a third `WayFilter` variant — rail only, the exact complement of `ExcludingRail` through `road_texture::is_rail_kind`, so the two cannot drift (`crates/geo/src/furniture.rs:363-368`). `half_width` answers `None` for it (a rail is not a carriageway), as does the `#[cfg(test)]` `push_line` arm (`crates/geo/src/furniture.rs:504-517`). - Add one query beside `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, or `None`. It reuses `nearest`'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.rs`** - `place_crossings` builds a second field from the same `TileData` with the new filter. It is behind the existing `points.is_empty()` early return (`crates/geo/src/crossings.rs:309`), so a cell with no registered crossing pays nothing, and the furniture's `WayFilter::All` path is untouched. - `place_crossings_against` takes both fields and records the measured span on `LevelCrossing` as a new `Option<f32>` field, in metres, measured with the yaw it just resolved — the same "one lookup, one road" rule `road_half` already follows (`crates/geo/src/crossings.rs:298-302`). `None` when nothing crossed. - `push_crossing` combines the two sources instead of replacing one with the other: ``` let ceiling = clearance_m(MAX_TRACKS as u8); // 37.25 m, today's own cap let measured = span.map_or(0.0, |s| s + SETBACK_M); let clearance = clearance_m(crossing.tracks).max(measured).min(ceiling); ``` and the span query is asked for `ceiling` as 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. `max` is 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_width` returns either of two opposite yaws and says so (`crates/geo/src/furniture.rs:573-576`, restated at `crates/geo/src/crossings.rs:268-272`); anything built on it must be symmetric under a half turn. So the measured span is a single `max |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`: the `aantal_sporen` census 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 at `crates/geo/src/crossings.rs:284-302` needs the same. ## Acceptance criteria - [ ] `WayFilter` has a rail-only variant defined as the complement of `ExcludingRail` via `road_texture::is_rail_kind`, with no second list of kinds. - [ ] A tile-side test proves the new filter reads the `kind` tag, on the pattern of `the_tile_side_filter_reads_the_kind_tag` (`crates/geo/src/crossings.rs:702-729`): a synth `streets` layer whose only feature is `kind = rail` is found by the rail-only field and not by `ExcludingRail`. - [ ] The reported case, synthesised: one N–S road, three E–W rails at 0, +5.9 m and −6.8 m along it, `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 off `build_crossing_meshes` output, not recomputed, the way `across` does it (`crates/geo/src/crossings.rs:518-543`). - [ ] A rail running **parallel** to the road within the search radius does not move either installation. - [ ] With no rail in the field, every form is placed exactly where it is today — the compatibility half, on the terms `an_unknown_width_falls_back_to_the_old_numbers` states (`crates/geo/src/crossings.rs:990-1010`). - [ ] The register still wins when it is wider: a single rail line in the field with `tracks: 4` keeps `clearance_m(4)`. - [ ] The ceiling holds: rails 200 m apart on either side cannot place an installation further out than `clearance_m(MAX_TRACKS as u8)`, so `the_track_count_is_bounded_at_both_ends` (`crates/geo/src/crossings.rs:863-869`) keeps its meaning. - [ ] The same crossing meshed at `yaw` and at `yaw + π` produces the same bounding box, pinning that the unresolved yaw sign is still safe. - [ ] `docs/notes/prorail-level-crossings.md` and the `crossings.rs` module note state that the spacing is measured from the tile's rail with the register as a floor, and no longer say `aantal_sporen` sets it alone. - [ ] No new Bevy dependency in `crates/geo`, and no change to `MAX_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 = rail` features near the register point: ```bash curl -s https://tiles.cartopolis.org/tiles/osm/14/8490/5321 -o /tmp/peizerweg_z14.mvt ``` then decode it (a `#[test]` over the bytes, or the throwaway script style used to read `groningen_z14.mvt` above). 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 as `crates/geo/tests/fixtures/peizerweg_z14.mvt` (LFS-tracked by extension, like the existing `.mvt` fixtures) and pin the real span in a test. Suite: ```bash cargo test -p cartopolis_geo crossings cargo test -p cartopolis crossings cargo fmt -p cartopolis -p cartopolis_geo -p cartopolis_core -p cartopolis_simulator -p cartopolis_android --check ``` The picture: ```bash cargo run -p cartopolis -- --shot /tmp/peizerweg.png \ --at 53.210238,6.549328,25 --look=-90,0 --dump-state /tmp/peizerweg.json ``` `level_crossings` must 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 - **Asymmetric installations.** The two stand at `±clearance` and stay symmetric, because the yaw sign is unresolved by construction. Placing each mast at its own side's rail would first need `direction_and_width` to answer which way the road points. - **The over-count direction.** A crossing registering 14 tracks where the road crosses two still spreads its masts 37 m apart. `max` cannot narrow that, and nothing here makes it worse. - **Filtering rail by `service`.** A `service = crossover` or 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. - **A `--dump-state` field for the clearance.** There is no metric for it today and none is added; the gate stays the unit tests plus the capture. - **The St Andrew's cross, animation, colliders, a Layers toggle of its own** — all four are standing "not drawn, and why" entries (`docs/notes/prorail-level-crossings.md:218-228`). - **Anything about the register's other six collections**, including `spooras` track axes — rejected with reasons at `docs/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-rails` <details><summary>Original request</summary> ## 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 then measured 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_sporen` and from nothing else: ```rust fn clearance_m(tracks: u8) -> f32 { let tracks = u32::from(tracks).clamp(1, MAX_TRACKS) as f32; (tracks - 1.0) * TRACK_SPACING_M * 0.5 + SETBACK_M } ``` The Peizerweg feature reports `aantal_sporen: 1` — verified against the live service today, and pinned by `the_real_answer_reads_as_one_half_barrier`. So clearance is 3.5 m and the two installations stand 7 m apart, straddling one track. The road crosses more than one. Overpass, around the register's own point: ``` way 399546810 railway=rail 0.0 m (the register point sits on it) way 550964277 railway=rail 5.9 m north way 636652195 railway=rail (yard) 6.8 m south ``` plus four separate `railway=level_crossing` nodes, every one of them `crossing: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 from `CrossingPoint::tracks`. `place_crossings` already builds a `WayField` that knows which ways are track — it is built with `WayFilter::ExcludingRail` precisely to keep the rail out of the orientation — so the same `TileData` can 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_m` already has to clamp away. <sub>Filed from the QA pass on #216 (#230).</sub> </details> <sub>🤖 Refined by the viberfox issue agent. Reply with **@agent refine** and what is wrong to have this rewritten.</sub>
Author
Collaborator

🤖 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.

🤖 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.
Author
Collaborator

🤖 Promoted into the build lane by the 7-day retrospective (#279) — autonomous + ship.

Why this one.

  • The defect is measured, not judged. The ticket already carries the three
    railway=rail ways at 0.0 m, 5.9 m and 6.8 m from the register's own point, against
    aantal_sporen: 1 and the 3.5 m clearance that follows from it. Whether a barrier
    stands clear of the outermost rail is a vertex position — value 6, and geometry is
    evidence on this hardware.
  • There is no design left in it. place_crossings already builds a WayField with
    WayFilter::ExcludingRail, so the rail span is in hand at the point clearance_m is
    called. 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).
  • The family is warm. #231 and #233 are the same module, were promoted by the last
    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.
  • Value 1. A surveyed count that the register itself gets wrong is being trusted over
    a measurement already loaded in the same function.

Bounds. crates/geo/src/crossings.rs. Keep aantal_sporen as the fallback so a cell
whose 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 just
landed.

🤖 **Promoted into the build lane** by the 7-day retrospective (#279) — `autonomous` + `ship`. **Why this one.** - **The defect is measured, not judged.** The ticket already carries the three `railway=rail` ways at 0.0 m, 5.9 m and 6.8 m from the register's own point, against `aantal_sporen: 1` and the 3.5 m clearance that follows from it. Whether a barrier stands clear of the outermost rail is a vertex position — value 6, and geometry is evidence on this hardware. - **There is no design left in it.** `place_crossings` already builds a `WayField` with `WayFilter::ExcludingRail`, so the rail span is in hand at the point `clearance_m` is called. 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). - **The family is warm.** #231 and #233 are the same module, were promoted by the last 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. - **Value 1.** A surveyed count that the register itself gets wrong is being trusted over a measurement already loaded in the same function. **Bounds.** `crates/geo/src/crossings.rs`. Keep `aantal_sporen` as the fallback so a cell whose 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 just landed.
Author
Collaborator

🤖 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:skip stops me.


🤖 viberfox issue agent on the VPS — 24 turns · $2.93 · session ff31c662-bbd4-4b9a-975b-2a66aeb8fcfc. Reply with @agent to continue this session.

🤖 **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:skip` stops me. --- <sub>🤖 viberfox issue agent on the VPS — 24 turns · $2.93 · session `ff31c662-bbd4-4b9a-975b-2a66aeb8fcfc`. Reply with **@agent** to continue this session.</sub>
Author
Collaborator

🤖 Picked this up — working on branch fix/232-crossing-clearance-from-rails in a fresh worktree on the VPS. I'll report back here when I'm done.

Label an issue agent:skip to keep me off it, or driven to keep it in a session you are driving.

🤖 Picked this up — working on branch `fix/232-crossing-clearance-from-rails` in a fresh worktree on the VPS. I'll report back here when I'm done. <sub>Label an issue `agent:skip` to keep me off it, or `driven` to keep it in a session you are driving.</sub>
Author
Collaborator

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 existing ExcludingRail arm, so there is one list of rail kinds and not two. Added WayField::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 that nearest already did is now a shared helper; nearest behaves exactly as before.
  • crates/geo/src/crossings.rs: place_crossings builds a second field over the same tile with that filter and records the measured span on each crossing. aantal_sporen is 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 saying aantal_sporen sets the spacing is corrected, and a new section records the measurement, the max rule 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 --check over the five owned crates: clean.
  • The capture does work in this container — the global note that it does not is stale, lavapipe is installed. Straight down over the crossing, level_crossings is 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: false with 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-rails is pushed.

Branch fix/232-crossing-clearance-from-rails (pushed)

10bcb20 fix(geo): measure a crossing's spacing from the tile's rail, not the register

🤖 viberfox issue agent on the VPS — 78 turns · $9.26 · session 5d628352-db0d-46fa-9b50-36a8b2f6fec2. Reply with @agent to continue this session.

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 existing `ExcludingRail` arm, so there is one list of rail kinds and not two. Added `WayField::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 that `nearest` already did is now a shared helper; `nearest` behaves exactly as before. - `crates/geo/src/crossings.rs`: `place_crossings` builds a second field over the same tile with that filter and records the measured span on each crossing. `aantal_sporen` is 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 saying `aantal_sporen` sets the spacing is corrected, and a new section records the measurement, the `max` rule 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 --check` over the five owned crates: clean. - **The capture does work in this container** — the global note that it does not is stale, lavapipe is installed. Straight down over the crossing, `level_crossings` is 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: false` with 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-rails` is pushed. **Branch** `fix/232-crossing-clearance-from-rails` (pushed) ``` 10bcb20 fix(geo): measure a crossing's spacing from the tile's rail, not the register ``` --- <sub>🤖 viberfox issue agent on the VPS — 78 turns · $9.26 · session `5d628352-db0d-46fa-9b50-36a8b2f6fec2`. Reply with **@agent** to continue this session.</sub>
Author
Collaborator

🤖 Marked agent:done without a CI verdict on 10bcb20f — no workflow reported on this commit. The branch is waiting for review; check the run yourself before landing it.

🤖 Marked `agent:done` **without a CI verdict** on `10bcb20f` — no workflow reported on this commit. The branch is waiting for review; check the run yourself before landing it.
Author
Collaborator

🤖 Not shipping this one — the branch conflicts with main (another ticket landed first).

Rebase fix/232-crossing-clearance-from-rails onto main, resolve, push, and put the ship label back to land it.

🤖 **Not shipping this one — the branch conflicts with `main`** (another ticket landed first). Rebase `fix/232-crossing-clearance-from-rails` onto `main`, resolve, push, and put the `ship` label back to land it.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeroen/cartopolis#232
No description provided.