RVO crop parcels (BRP), PDOK OGC API: is it worth wiring in? #203
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#203
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 first half of this ticket was to decide whether the source is worth wiring in. It is. The check below was run on 2026-08-30 against the live service.
The interface exists and is free.
https://api.pdok.nl/rvo/gewaspercelen/ogc/v1(the frontier row's guess at the path was wrong; there is nobrpgewaspercelenunder/rvo/). One collection,brpgewas, OGC API Features, keyless,bboxquery, GeoJSON out, storage CRS EPSG:28992 with CRS84 offered. The collections document names its own licence:https://creativecommons.org/publicdomain/mark/1.0/deed.nl— Public Domain Mark 1.0, so caching and redistributing what we serve needs nobody's agreement. One year is served (jaar: 2025,status: "Definitief", snapshot date 15 May) with no superseded versions in the response, so this does not have the BGT's ghost-version trap (crates/cartopolis/src/systems/map/surveyed.rs:148-165).Measured volume, per exact z14 cell (the zoom
map_geometrystreams at,crates/cartopolis/src/systems/map/surveyed.rs:36):Zero inside the city and zero outside the Netherlands is the degrade-to-nothing shape value 3 asks for. Douglas–Peucker at 0.5 m keeps 31–32 % of the crop vertices (5,206 and 1,270 respectively) — call it ~40 KB of coordinates for the heaviest cell measured, against the 3.8 MB of surveyed paving that is already the second-heaviest thing streamed. Volume is not an obstacle.
It changes the picture, and the reason is in this repo's own code.
land_groupcollapsesgrass,meadow,park,garden,farmland,orchard,scrub,heath,allotments,village_greenandrecreation_groundinto oneSurfaceGroup::Grass(crates/geo/src/vector_tiles.rs:1411-1412), which paints one flatpalette::GRASS(crates/geo/src/vector_tiles.rs:1224, and:549on the raster path). That is literally the "one generic green fill" the row describes. Over the Flevoland cell, 93 % of the parcel area isBouwlandcarrying 33 distinct crops — pears, consumption potatoes, yellow onions, winter wheat, sugar beet, maize — and today all of it is the same green as a city park.And it fixes a value-1 defect that already ships.
GrassArea::buildgrows near-field grass blades on every polygonland_groupcallsGrass(crates/geo/src/turf.rs:104-131), farmland included — so a Dutch potato field currently grows generated grass right now.Honest counterweight, and it goes in the note: over the Groningen farmland cell 94 % of the parcel area is
Grasland, i.e. green repainted as green. About half the country's agricultural area is grassland, so half the countryside gains little beyond a more accurate green. That is not a reason to refuse — a register that confirms the guess is still the register — but nobody should expect the whole country to change.Two things the check found that the implementation must handle and that are not obvious:
Landschapselement, and 316 of 340 of those sampled over the wider box areSloot— ditches. Those are water. The Shortbreadwater_polygons/water_lineslayers already draw them (crates/geo/src/vector_tiles.rs:562-570), and drawing a ditch as a land fill puts land where the register says water. Categories seen across four sampled regions:Landschapselement1616,Grasland1428,Bouwland728,Natuurterrein226,Braakland1,Overige1.surveyed_pavingdrops any surface whose outer ring leaves its cell (crates/cartopolis/src/systems/map/surveyed.rs:207-215, viato_cell_local's containment test at:371). Measured: 16 % of crop parcels over Groningen farmland and 31 % over Flevoland cross a z14 boundary. Copying that rule would delete a third of Flevoland's fields.The frontier row's own figures (163 parcels / 501 KB) do not reproduce on a z14 cell and should be corrected to the table above.
Approach
Follow the surveyed-layers shape exactly: an extractor binning into z14 cells, a coverage manifest, per-cell blobs consumed inside
map_geometry's existing worker (not a streamer of its own — the polygons are merged into that cell's meshes, the reasoncrates/cartopolis/src/systems/map/surveyed.rs:20-24gives), and byte-bounded meshes through the samemesh_bytesceiling (crates/cartopolis/src/systems/map/map_geometry.rs:442).The extractor goes in cartopy, not
tools/. The ticket text saystools/; that instruction is stale — the four Dutch extractors moved tojeroen/cartopyunderserver/pipeline/(docs/notes/where-the-map-data-is-built.md). This ticket is the client half, which is the shapedocs/direction.md:97already records for the BGT's second round: the client goes first, because a client that can name a form the host never sends is harmless while the reverse is silently dropped content.crates/geo— the pure halfNew
crates/geo/src/crops.rs, modelled oncrates/geo/src/paving.rs:CropParcel { category, code, rings }in cell-local metres, rings open, outer first.CropCategory—Grassland,Arable,Nature,Fallow,Other. Append, never renumber, mirroringPavingKind's contract (crates/geo/src/paving.rs:55-57).Landschapselementgets no variant: it is not drawn, for the ditch reason above.rgb(category, code) -> u32: a curatedgewascode→ tone table for the crops that cover real area, falling back to the category's tone for any code not listed. An unlisted arable code draws as generic arable, never as a neighbouring crop's colour — the same discipline asland_group's "anything unlisted is skipped rather than guessed" (crates/geo/src/vector_tiles.rs:1408). Per-parcel tone rides in vertex colours so the layer stays one group, one material, one mesh per cell, exactly asPaving,CanopyandFurnituredo (crates/geo/src/vector_tiles.rs:1231-1234).build_crop_meshes(&[CropParcel], mesh_bytes) -> Vec<SurfaceMesh>, mirroringpaving::build_paving_meshes.New
SurfaceGroup::Cropincrates/geo/src/vector_tiles.rs:base_color→0xFF_FF_FF(vertex colours carry the tone),perceptual_roughnessunchanged at 0.95.y_offset→ −0.0080, betweenGrassat −0.0085 andForest | Sandat −0.0075 (crates/geo/src/vector_tiles.rs:1284-1289). Half the usual millimetre because a crop parcel only ever overlapsGrass; it is never coplanar with a wood or a beach.depth_rung→ 2.5, betweenGrass2.0 andForest | Sand3.0 (crates/geo/src/vector_tiles.rs:1345-1349). AtDEPTH_BIAS_STEP = 64.0(:310) that is 32 ULP of margin — half the ladder's normal step, still far above the rasteriser's one-ULP noise floor, and it keeps every parcel under water (rung 4), under paving (5) and under the road band.crates/geo/src/turf.rs:GrassArea::buildgains an arable keep-out, in the same shape as theWaterMaskandRoadMaskit already carries (crates/geo/src/turf.rs:129-131). Blades do not grow on aCropCategory::Arableparcel.crates/cartopolis— the streaming halfNew
crates/cartopolis/src/systems/map/crops.rs, modelled onsurveyed.rs:static COVERAGE: CoverageSlot = CoverageSlot::new("brp", "/brp/coverage.json", 14)— the shared manifest reader atcrates/cartopolis/src/systems/map/coverage.rs:52, whose latch-after-three-attempts policy (:29-31) is exactly right for an additive tree.prefetch_brp_coverageregistered besidesystems::surveyed::prefetch_bgt_coverage(crates/cartopolis/src/lib.rs:2207), andcoverage_revision()folded intoMapSurfaces's rebuild revision besidebgt_revision(crates/cartopolis/src/systems/map/map_geometry.rs:306-311) so cells built during the manifest gap are retorn.parse_crop_cell(text)over{"parcels":[[category,code,[ring,…]],…]}— the positional-array wire shapeparse_surveyed_pavinguses (crates/cartopolis/src/systems/map/surveyed.rs:139-148), with lat/lng pairs and the same exact-duplicate fold.load_crop_cell(cx, cy)fetching{tdbag_url()}/brp/14/{cx}/{cy}.jsonand caching underbrp/v1/14/{cx}/{cy}.json, mirroringcrates/cartopolis/src/systems/map/surveyed.rs:308-331.crop_parcels(&cell, cx, cy, tile_m)projecting to cell-local metres. This needs an unclamped variant ofto_cell_local(crates/cartopolis/src/systems/map/surveyed.rs:368-386): same Mercator-y maths, without the containment test at:371. A parcel is binned by the extractor on its centroid and drawn whole by that one cell, vertices outside the cell included — the 16–31 % straddle measurement above is why.brp_attribution()besidebgt_attribution()(crates/cartopolis/src/systems/map/surveyed.rs:62), rendered in the credits block atcrates/cartopolis/src/systems/gui/data_layers.rs:2507-2524.crates/cartopolis/src/systems/map/map_geometry.rs:load_crop_cellawaited besideload_surveyed_pavingat:455, and the meshes built in the worker beside the paving at:500-505. No Layers-panel toggle — surveyed paving has none either.Metric:
CellPlaces::surveyedbecomes a 5-tuple (crates/cartopolis/src/systems/map/world_places.rs:52,:281,:346,:374) andShotMetricsgainscrop_parcels, populated besidesurveyed_pavingatcrates/cartopolis/src/systems/dev/shot_harness.rs:2637.Documentation
docs/notes/crop-parcels.mdwith the front-matter formatdocs/notes/README.mddescribes: the endpoint, the licence, the per-cell measurements above with their date and method, the ditch decision, the straddle measurement and why the paving binning rule was not copied, and the honest limit that grassland is repainted its own colour.docs/direction.md: move the row from Candidates, unverified (:81) into Wired (:73-78), correcting the volume figures.Acceptance criteria
crates/geo/src/crops.rsexists withCropParcel,CropCategory(an append-only list documented as such) andbuild_crop_meshes, and nobevydependency.CropCategoryhas no variant forLandschapselement; the parser drops that category, and a test asserts a ditch record produces no parcel.SurfaceGroup::Cropexists withy_offset−0.0080 anddepth_rung2.5, and a test asserts its rung lies strictly betweenGrassandForestand strictly belowWater.gewascodenot in the curated tone table falls back to its category's tone; a test asserts an unknown arable code draws as generic arable and never as another crop's colour.parse_crop_cellreads the positional wire form, drops an outer ring under three points, and folds exact duplicates — tests in the shape ofcrates/cartopolis/src/systems/map/surveyed.rs:513-546.mesh_bytesceiling as paving and vegetation (crates/cartopolis/src/systems/map/map_geometry.rs:442); no new per-frame upload path is introduced.CoverageSlot::new("brp", "/brp/coverage.json", 14)is declared, prefetched from a system registered besideprefetch_bgt_coverage, and its revision is folded intoMapSurfaces's rebuild revisions.GrassArea::buildgrows no blades on aCropCategory::Arableparcel; a test asserts blades in a grassland parcel and none in an arable one covering the same ground.ShotMetrics::crop_parcelsappears in--dump-state, and an unknown--expectkey remains a startup error.brp_attribution()renders in the credits block when coverage is non-empty.docs/notes/crop-parcels.mdexists with the endpoint, the Public Domain Mark 1.0 licence, the three measured cells with their date, and the grassland-repaints-green limit.docs/direction.mdrow has moved to Wired with the corrected figures.jeroen/cartopyforserver/pipeline/brp.tsand itsformat.test.tsentry, quoting the wire format above verbatim.Verification
In this container (the implementing session, not this pass):
Run bare
cargo fmt --check— the CI gate is workspace-wide.Re-running the volume check needs no build:
Not verifiable here, and not verifiable anywhere until the cartopy half ships. A scripted capture over farmland asserting
--expect 'crop_parcels>0'needs a host serving/brp/coverage.json; until then the metric reads 0 everywhere and the tests above are the whole machine gate. Once cartopy serves a cell:The
--flickerrun is the one that matters for the new depth rung: 32 ULP is half the ladder's normal step, and a coplanar fight over a whole province is exactly the failure this instrument exists for. It renders here on lavapipe, so it can be run in the container — but the resulting colours are not evidence (docs/notes/headless-shots-software-renderer.md), so whether the crop palette actually reads as a patchwork of fields is a workstation judgement.Out of scope
server/pipeline/brp.ts, the/brp/coverage.jsonmanifest generation, the 0.5 m Douglas–Peucker simplification and the centroid binning all live injeroen/cartopy, a separate repository with its own CI. This ticket pins the wire format and files the follow-up; it does not write the TypeScript.farmlandandmeadowtones of their own. The client throws away a distinction the tiles already carry, which would improve the countryside outside the Netherlands for nothing. It is a real ticket and it is not this one — BRP names the crop, which OSM never does.jaar. Flat fills only.docs/shots.tomlpreset over farmland. Worth having once there is data to photograph; not part of this change.Landschapselement. The ditches, hedgerows and copses are 60 % of the features and are already registered in the BGT and drawn from the Shortbread water layers.Open questions
None. The three decisions that could have been asked were resolved from
docs/direction.md's values and are recorded here for the commit body: drawing the crop fill over the existing green rather than replacingSurfaceGroup::Grassis value 4; droppingLandschapselementbecause the ditches are already drawn as water is value 4 again; and shipping the client half alone behind acrop_parcelscount and unit tests, rather than waiting on the other repository, is value 6 plus the precedentdocs/direction.md:97already records for the BGT.Sources: PDOK BRP OGC API · PDOK dataset page
Branch:
feat/203-brp-crop-parcelsOriginal request
From the frontier in
docs/direction.md: RVO crop parcels (BRP), PDOK OGC API — Every agricultural field with the crop grown on it that year, so the country outside the cities stops being one generic green fill: 163 parcels and 501 KB over a farmland cell north of Groningen, 131 parcels and 149 KB over Flevoland arable land, and zero features inside the city, which is the degrade-to-nothing shape the other streamers already have..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 — 48 turns · $4.25 · session
80050faf-233e-4f40-a25b-80862f723094. Reply with @agent to continue this session.🤖 Picked this up — working on branch
feat/203-brp-crop-parcelsin 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.Farmland is now drawn as the crop grown on it instead of one flat green — the client half of the RVO crop register (BRP). It draws nothing yet: no host serves the data until the extractor exists (issue #204).
What changed
New, in
crates/geo:crops.rs—CropParcel, an append-onlyCropCategory(Grassland,Arable,Nature,Fallow,Other; no variant forLandschapselement), a table mapping about 60 crop codes to fourteen tones, andbuild_crop_meshes. An unlisted code draws as its category, never as a neighbouring crop.flat_fill.rs— the triangulation, moved out ofpaving.rsso both layers share one copy of the winding fix, the byte cap and the colour conversion.paving.rsnow calls it and behaves as before.SurfaceGroup::Cropat rung 2.5, height −0.0080: above grass, below wood, water, paving and roads.GrassArea::buildtakes the cell's parcels and refuses to grow blades on arable ones. That fixes something that ships today — OpenStreetMap files farmland as grass, so Dutch potato fields currently grow generated lawn.New, in the client:
systems/map/crops.rs— a coverage manifest at/brp/coverage.json, per-cell files at/brp/14/{x}/{y}.json, and the projection into the frame the meshes are built in. Parcels are built inside the existing tile worker, beside the surveyed paving, and go through the same byte ceiling.crop_parcelsis a new--dump-statefield; the credits panel gains a line when coverage loads.Documentation:
docs/notes/crop-parcels.md, and the frontier row moved to "Wired" indocs/direction.md.Two decisions worth knowing
How I verified it
cargo test -p cartopolis_geo(219 passed),cargo test -p cartopolis(794 passed),cargo check -p cartopolis, andcargo fmt --checkclean across the workspace. Twenty-one of those tests are new. I re-ran the volume check against the live service on 2026-08-30 and the ticket's three-cell table reproduces exactly.To build the client here at all I had to install ALSA sound headers into my home directory — this container has neither the headers nor the library, and
--no-default-featuresfails with ten unrelated dead-code errors. Recorded in memory.Not verified
--flickercheck over farmland is what would confirm it, and it needs the extractor first.For you to decide
I could not file the follow-up ticket against
jeroen/cartopy: from this container that repository answers 404 to the token and "Repository not found" to git. I filed it as cartopolis issue #204 instead, with the complete wire format, and said in the body that it belongs in the other repository. Please move it or hand it to a session that has the credentials.Branch
feat/203-brp-crop-parcels(pushed)🤖 viberfox issue agent on the VPS — 136 turns · $19.86 · session
3f60ff4b-77fc-4a71-a210-355c257369dd. Reply with @agent to continue this session.🤖 Marked
agent:donewithout a CI verdict on6e7cb309— no workflow reported on this commit. The branch is waiting for review; check the run yourself before landing it.🤖 Merged into
mainas pull request #205 (6e7cb309).🤖 Deployed to the public services.
agent:donenever means "CI agreed" #220