Rijkswaterstaat vaarwegmarkeringen, PDOK OGC API: is it worth wiring in? #206
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#206
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:82lists Rijkswaterstaat's vaarwegmarkeringen as an unverified frontier candidate. The first half of this ticket was to confirm the interface, the licence and the volume. All three check out, and the row's description is accurate.Interface.
https://api.pdok.nl/rws/vaarwegmarkeringen-nederland/ogc/v1(the URL the row implies,…/rws/vaarwegmarkeringen/…, 404s). Keyless OGC API Features, two collections —vaarweg_markeringen_drijvend_rd(floating: buoys) andvaarweg_markeringen_vast_rd(fixed: beacons, groyne marks, shore lights).bboxquery, cursor paging,limitcapped at 1000,Access-Control-Allow-Origin: *,Cache-Control: public, max-age=3600.properties=subsetting is not supported (400), so the full ~40-property feature comes down.Licence. CC0 1.0, stated by the service's own
licenselink. No obstacle.Volume, measured 2026-08-30 over the national bbox
3.0,50.6,7.4,55.7:Per z14 cell (the zoom
map_geometrystreams at —surveyed.rs:35pinsBGT_ZOOM = 14tomap_geometry::GEOMETRY_ZOOM):Attributes, censused over 625 floating and 267 fixed marks across five boxes (IJsselmeer, het IJ, Rotterdam, the Waal, Hollands Diep):
obj_vorm— 4 values:spar426,stomp127,spits65,bol7 (spar / can / conical / spherical).obj_kleur— 13 values, all of the formA,A/B,A/B/AorA/B repeterendover {Rood, Groen, Geel, Wit, Zwart, Grijs}:Geel169,Rood113,Groen102,Rood/wit repeterend101,Groen/wit repeterend93, then nine tails down to 1.tt_toptek/tt_kleur— topmark, present on 317/625:Cilinder189,Kegel, punt naar boven75,Liggend kruis17, four two-cone forms,Bol6.licht_klr— a light colour on 166/625 floating marks (Rood 63, Groen 55, Geel 28, Wit 20);licht_klon 133/267 fixed.sign_kargives the character (Iso 77, LFl 45, Fl 28, Q 7 …) andsign_periothe period in seconds, on the same subset.naut_functinstead of a shape:Kribbaken121,Bermverlichting39,Oeverlicht32,Havenlicht19,Lichtopstand19,Lichtenlijn14, down toLichttoren (vuurtoren)2.obj_hoogteis0,00000on all 267 fixed marks — the register carries no object height.licht_hgtis the light's height above water and is0,0on 183/267.opgeheven(decommissioned) is#on all 892 sampled — unlike the BGT, this API is not returning historical versions.Worth doing, and the decision was made from the values in
docs/direction.md:24-46. Value 1 (measured beats plausible): the only thing standing on open water in this client today is the inferred moored boats ofcrates/geo/src/moorings.rs:200-204, which the notedocs/notes/canal-moorings.mdis explicit about being a guess from polygon shape. A buoy is surveyed, individually, with its colour. Value 2: PDOK, CC0, keyless. Value 3: zero features over Groningen, Amsterdam centre and Utrecht centre, which is the degrade-to-nothing shape the streamers already have. Value 4: it adds a form; nothing is replaced. Value 5: 43 marks in the densest cell measured, merged into an existing per-cell mesh — no new upload path. Value 6: the count is a--dump-statefield.The counter-case, and why it does not win: the four cities the client is usually flown around have almost nothing (Rotterdam's Nieuwe Maas cell has one mark, Amsterdam's IJ cell two). This is not the
docs/notes/ndw-traffic.mdshape, where the measured objects fell nowhere near where anybody stands — the Waal at Nijmegen has 43 in a single 1.5 km cell and the IJsselmeer, the Amer, the Noordzeekanaal and Hollands Diep are dense. It is a layer that appears on the water and nowhere else, which is what it should be.Approach
Two crates, no cartopy change, no new database.
Where the data comes from — a decision that departs from the ticket boilerplate. The ticket text asks for "an extractor under
tools/, a coverage manifest". That is the BGT/BAG/dikes shape, and it is wrong for this source.docs/notes/where-the-map-data-is-built.mdmoved the four Dutch extractors to cartopy, and the repo's own CLAUDE.md warns that "nothing but a person keeps the two repositories in step". A cross-repo seam is worth paying for a 2 MB-per-cell dataset that needs reprojection and triangulation; it is not worth paying for 18,474 points behind a keyless bbox query with CORS*and an hour of cache control. The in-tree precedent for fetching PDOK directly from the client iscrates/cartopolis/src/systems/map/height.rs:53(AHN) andcrates/cartopolis/src/systems/map/aerial.rs:39; the precedent for a per-cell third-party streamer iscrates/cartopolis/src/systems/map/tall_structures.rs:1-16against Overpass. Follow those. NoCoverageSloteither —crates/cartopolis/src/systems/map/coverage.rs:1-9answers "which cells does our host serve", which does not apply here; coverage is an envelope, exactly asaerial.rsdecided for the same reason. Say all of this in the commit body.crates/geo/src/nav_marks.rs(new, Bevy-free, unit-tested):NavMark { x: f32, z: f32, kind: MarkKind, body: ColourPattern, topmark: Option<Topmark>, light: Option<MarkColour> }in cell-local metres.surveyed.rs:71-78states forPROP_KINDS.obj_kleuris parsed by splitting on/and stripping a trailingrepeterend, soRood/wit repeterendandGeel/zwart/geelboth resolve without a 24-entry lookup table.SurfaceGroup::Furniture(crates/geo/src/vector_tiles.rs), so marks inherit the group's shared material, its altitude fade (furniture.rs:60-64,FURNITURE_FADE_START150 m /FURNITURE_FADE_END450 m) and its Layers-panel toggle for free.FurnitureKindvariants.FurnitureInstance(crates/geo/src/furniture.rs:84-102) carries exactly onepaint: u32, and a mark's colour is a pattern of up to three bands plus a separate topmark colour plus a light colour. Widening that struct would touchparking,trafficandmooringsto add a field only one form reads.y = 0in the cell frame with the body straddling it, exactly asfurniture::form::BOATdoes (docs/notes/canal-moorings.md, "the hull sits 20 cm below y = 0 … that is deliberate: y = 0 is where the water surface is").FurnitureKinds, socollision::sync_prop_collidersnever sees them — which is the answermooringsreaches by a different route (prop_radius= 0 for a boat).crates/cartopolis/src/systems/map/nav_marks.rs(new, the fetch half):async fn load_marks_cell(cx, cy) -> MarkCell, shaped ontall_structures::fetch_cell_text(tall_structures.rs:114-137): cache keyfeatures/marks/v1/14/{x}/{y}.jsoninstorage::Namespace::Cache, cache hit short-circuits, miss fetches both collections'/items?f=json&limit=1000&bbox=…and stores the compacted result rather than the raw 940-bytes-per-feature GeoJSON.map_geometry's existing per-cell job at the pointsurveyed::load_surveyed_cellis awaited (crates/cartopolis/src/systems/map/map_geometry.rs:452), for the reason that module gives at its lines 20-24: this is baked into a merged mesh, so it must be known on the worker that builds it.extent.spatial.bboxis2.354, 50.714, 7.555, 55.669), the same "coverage is an envelope, never a status code" ruleaerial.rsfollows. Second, the cell's water:map_geometryhas already parsed the tile at this point, and a cell whose tile carries no water geometry cannot contain a mark. Together these keep an inland or foreign cell at zero requests instead of two 1.9 KB empty answers.furniture.rs:71): a page that comes back withnumberReturned == 1000means the cursor has more, and at z14 it never should.Wiring:
crates/geo/src/lib.rs—pub mod nav_marks;crates/cartopolis/src/systems/map/mod.rs—pub mod nav_marks;plus the flat re-export insystems/mod.rs(CLAUDE.md: the folders are not the import path).map_geometry.rs— call the mesher where the surveyed props are added (map_geometry.rs:575-590), count into the per-cell readout besidecell_places.surveyed.shot_harness.rs— onepub nav_marks: usizefield onShotMetrics(besidesurveyed_propsat line 601), the CSV header at line 821, and the assignment beside line 2635.docs/notes/navigation-marks.md— new note with the measurements above, theproperties=400, the missingobj_hoogte, and why there is no extractor and no coverage manifest.docs/direction.md— move the row from Candidates, unverified into the Wired list.Acceptance criteria
crates/geo/src/nav_marks.rsexists, has nobevydependency, and parsesobj_vorm,obj_kleur,tt_toptek,tt_kleur,licht_klr/licht_klandnaut_functfrom the vocabularies listed above, dropping any value it has no form for rather than substituting one.obj_kleurparsing covers all 13 floating and 11 fixed values observed, including theA/B repeterendandA/B/Aforms, driven by a test over the literal strings in the Problem section.opgehevenis anything other than#is dropped.SurfaceGroup::Furnitureand therefore ride the existing altitude fade and Layers toggle; no newDataLayersfield is added.storage::Namespace::Cachewith a versioned key, and a cache hit issues no network request.limitfeatures logs a truncation warning once.furniture::MAX_FURNITURE_PER_CELL(crates/geo/src/furniture.rs:75), and truncation is logged.ShotMetricsgainsnav_marks, present in theshot stateline, in--csvand in--dump-state.docs/notes/navigation-marks.mdrecords the measurements, the licence, the endpoint, and the two rejected alternatives (a cartopy extractor; a coverage manifest).docs/direction.mdmoves the row into Wired, and the note is linked.Verification
Not runnable in this pass — this is a read-only refinement, and the numbers below are the implementer's to produce.
The headless gate, on a machine where a build is allowed (the container's own
docs/notes/headless-shots-software-renderer.mdsays lavapipe renders geometry here trustworthily; this refinement pass may not run it):Only a workstation can judge whether the colours read correctly — colour and lighting are not evidence on a software rasteriser. The geometry half (count, placement on the water rather than the bank, the fade band) is judgeable here.
Re-measuring the source, if any number above is doubted:
Out of scope
licht_klr/sign_kar/sign_periogive a colour, a character and a period, and 27% of floating marks and 50% of fixed ones carry them — but the Furniture group is a sharedStandardMateriallit by vertex colour, with no per-object emissive anywhere incrates/geo/src/furniture.rs. A flashing navigation light is a second material or an emitter and is its own ticket. This ticket paints the lantern cap in the registered colour and stops there.obj_hoogteis0,00000on every fixed mark sampled, so there is no height to build one from;LichttorenandLichtopstandare drawn as the same beacon post as the rest. The tall-landmark lane issystems::tall_structuresand takes its heights from OSM.nationaal-wegenbestand-vaarwegenandvaarweg-netwerk-data-service-bevaarbaarheidare separate PDOK services and separate frontier questions.server/pipeline/change.furniture_enabledbecause they share the mesh group — the same reasoningdata_layers.rs:490-499gives for cars and benches sharing one toggle.Open questions
None.
Branch:
feat/206-navigation-marks-layerOriginal request
From the frontier in
docs/direction.md: Rijkswaterstaat vaarwegmarkeringen, PDOK OGC API — Every buoy and fixed beacon on the national waterways, each carrying its colour pattern and the colour and character of the light it shows, so a river or a canal holds the objects that are really standing in it: 5 floating and 3 fixed marks in 13 KB over a Hollands Diep cell..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 — 46 turns · $4.16 · session
a5ec152c-ae99-40e4-b6ec-f00b3a5b7226. Reply with @agent to continue this session.🤖 Picked this up — working on branch
feat/206-navigation-marks-layerin 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.The branch is pushed. Here is the comment for the issue.
Wired it in. The waterways now hold the buoys and beacons Rijkswaterstaat actually surveyed — shape, colour bands, topmark, and the lantern painted in the colour of the light it shows. Nothing else on the map changed: outside the Netherlands, away from water, or with the Furniture layer off, the client draws exactly what it drew before and makes no requests.
Two new files do the work:
crates/geo/src/nav_marks.rsreads the register's vocabularies and builds the meshes,crates/cartopolis/src/systems/map/nav_marks.rsfetches and caches a cell. The marks go into the existingSurfaceGroup::Furniture, so they inherit its material, its altitude fade and its Layers row without new wiring, and they get no collider — an invisible wall in a fairway is worse than a buoy you can walk through.Where I departed from the ticket, and why
map_geometryhas already parsed the tile at that point. It has not — the tile is fetched on the I/O pool and parsed later on a worker, and waiting for the parse would put a ~120 ms decode on the frame loop in the browser build. Sovector_tiles::body_mentions_watersearches the raw bytes for the water layer names. A layer that exists cannot fail to be named in the body, so a false negative is impossible; a false positive costs one request.Bermverlichting,Bordverlichting,Aanstraalverlichting,Walkast. Those are verge lighting, sign lighting, floodlighting and a shore cabinet, not navigation marks, and the BGT already surveys street lighting column by column, so drawing them would stand two lampposts on one spot. That is 33 of 260 sampled fixed features.Niet toegewezen).Three things in the data the ticket's census missed, all of which fail by producing nothing rather than by erroring:
MultiPoint, neverPoint. A reader matching onlyPointfinds zero marks nationally and looks exactly like a river with none.v_toptekandlicht_klwhere the floating one writestt_toptekandlicht_klr. Reading only the floating keys leaves every river beacon bare.obj_vormhas a fifth value,Pilaar(12 of 998 sampled).I put two real answers from the service under
crates/cartopolis/tests/fixtures/and test against them, so those three are pinned against the register rather than against my idea of it.Verified
cargo test -p cartopolis_geo— 223 passed, 14 of them new.cargo test -p cartopolis— 806 passed, 12 of them new.cargo fmt --checkacross the workspace, clean.cargo checkforcartopolis,cartopolis_simulator, and the web target with the nightly flags the deploy uses.Not verified
--shotin this container, so the two headless gates in the ticket are unrun. The commands are in the ticket andnav_marksis now a--dump-statefield, so--expect 'nav_marks>=30'over the Waal and--expect 'nav_marks==0'over Groningen will work as written.tall_structuresalready uses on that target.docs/notes/navigation-marks.mdhas the measurements, the licence and the two rejected alternatives; the row has moved into Wired indocs/direction.md.Branch
feat/206-navigation-marks-layer(pushed)🤖 viberfox issue agent on the VPS — 126 turns · $17.52 · session
8b075b37-96ae-460a-a81f-6e459875e374. Reply with @agent to continue this session.🤖 Marked
agent:donewithout a CI verdict on38c2387c— no workflow reported on this commit. The branch is waiting for review; check the run yourself before landing it.🤖 Merged into
mainas pull request #207 (38c2387c).🤖 Deployed to the public services.
agent:donenever means "CI agreed" #220alreadyFiledonly ever sees 50 issues, so every autopilot dedupe key silently expires — #206 was QA'd twice #281