Skip to content

Add AHN4 elevation map feature with COG integration#376

Merged
SvenVw merged 15 commits into
developmentfrom
FDM361-2
Dec 17, 2025
Merged

Add AHN4 elevation map feature with COG integration#376
SvenVw merged 15 commits into
developmentfrom
FDM361-2

Conversation

@SvenVw
Copy link
Copy Markdown
Collaborator

@SvenVw SvenVw commented Dec 15, 2025

Summary by CodeRabbit

  • New Features
    • Added an Elevation (AHN4) view in Atlas with toggleable elevation rendering and hillshade.
    • New Elevation legend with gradient scale, min/max labels, network/loading states and hover height display.
    • Map UI improvements: persistent view, raster tiles, field overlays, and hover-based elevation feedback.
    • Breadcrumb and sidebar updated for easy switching between Crop Parcels and Elevation Map.

✏️ Tip: You can customize this high-level summary in your review settings.

Closes #361

@SvenVw SvenVw self-assigned this Dec 15, 2025
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Dec 15, 2025

🦋 Changeset detected

Latest commit: d0f243c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@svenvw/fdm-app Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 15, 2025

Warning

Rate limit exceeded

@SvenVw has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 8 minutes and 20 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between ba8ef5f and d0f243c.

📒 Files selected for processing (1)
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx (2 hunks)

Walkthrough

Adds an AHN4 elevation feature to Atlas: new elevation route and map UI using a COG protocol, elevation controls and legend, breadcrumb/sidebar navigation updates, projection and spatial helpers, COG index caching, and two new dependencies for COG protocol and spatial intersection.

Changes

Cohort / File(s) Summary
Changeset
\.changeset/jolly-ravens-shop.md
Adds a changeset bumping @svenvw/fdm-app (minor) describing the AHN4 elevation layer implementation.
Atlas Controls & UI
fdm-app/app/components/blocks/atlas/atlas-controls.tsx
Adds elevation toggle (showElevation/onToggleElevation), introduces ElevationControl using a Mountain icon, refactors internal FieldsButtonControlButton, replaces CustomFieldsControl with CustomControl, and syncs control lifecycle via hooks.
Atlas Legend
fdm-app/app/components/blocks/atlas/atlas-legend.tsx
New ElevationLegend component rendering dynamic gradient scale, min/max labels, hover value display, loading/network states and optional message.
Header / Breadcrumb
fdm-app/app/components/blocks/header/atlas.tsx
Replaces static breadcrumb link with a dropdown menu to switch between Gewaspercelen and Hoogtekaart; uses NavLink and dropdown UI parts.
Sidebar / Navigation
fdm-app/app/components/blocks/sidebar/apps.tsx
Converts Atlas menu into a Collapsible with sub-items for Gewaspercelen and Hoogtekaart; derives links conditionally and sets active state by path inclusion.
Elevation Route / Map
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
New route: registers RD projection and COG protocol, fetches & caches kaartbladindex.json, adds spatial helpers (point-in-polygon, polygon intersection), computes visible tiles, samples elevations for dynamic min/max, integrates MapLibre map with COG raster + hillshade + WMS overview and GeoJSON field overlays, adds hover elevation queries and UI wiring (Controls, Legend, hover panel). Loader now returns { fields, mapStyle, calendar }; meta title updated.
Metadata / Titles
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx, fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx
Route meta titles updated to “Atlas” variants (e.g., "Hoogte - Atlas", "Percelen - Atlas").
Dependencies / Build
fdm-app/package.json, fdm-app/vite.config.ts
Adds @geomatico/maplibre-cog-protocol and @turf/boolean-intersects; adds the COG protocol package to Vite SSR noExternal.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as UI/Controls
    participant Map as MapLibre GL
    participant Loader as Route/Loader
    participant Index as Kaartbladindex (cached)
    participant PDOK as PDOK COG Service
    participant Legend as ElevationLegend

    User->>UI: Open Atlas → select Hoogtekaart
    UI->>Loader: Initialize elevation route
    Loader->>Index: Fetch kaartbladindex.json (localStorage cache)
    Index-->>Loader: Return tile index

    rect rgb(220,240,255)
    Note over Loader,Map: Initial viewport setup
    Loader->>Map: Set view + register cog protocol / RD projection
    Map->>Index: Request visible tiles (bounds → RD)
    Index-->>Map: Return intersecting tiles
    Map->>PDOK: Request COG tiles & hillshade (via cog://)
    PDOK-->>Map: Return raster tiles
    end

    rect rgb(240,220,255)
    Note over Loader,Legend: Dynamic scaling
    Loader->>PDOK: Sample overviews for visible tiles (min/max)
    PDOK-->>Loader: Return sampled elevation stats
    Loader->>Legend: Update min/max and color scale
    Legend-->>UI: Render gradient + labels
    end

    rect rgb(220,255,230)
    Note over User,Map: Interaction (hover/pan)
    User->>Map: Hover / Pan / Zoom
    Map->>PDOK: Query elevation at point or request tile samples
    PDOK-->>Map: Return elevation value(s)
    Map->>Legend: Update hoverValue / Loader: throttle & recalc stats
    Legend-->>User: Display "Hoogte: X m NAP"
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on: farm.$b_id_farm.$calendar.atlas.elevation.tsx (projection, tile filtering, COG integration, caching, performance/throttling), atlas-controls.tsx (control lifecycle and prop changes), and atlas-legend.tsx (scale math and edge cases).
  • Check dependency compatibility (@geomatico/maplibre-cog-protocol) and SSR noExternal handling.

Possibly related PRs

  • #67 — Related Atlas feature changes and map routes; overlaps in elevation/map-layer work.
  • #345 — Prior edits to atlas-controls.tsx touching ControlsProps and control composition.
  • #161 — Earlier refactor of atlas controls and control component patterns.

Suggested reviewers

  • gerardhros
  • BoraIneviNMI

Poem

🐰 I hopped through tiles and RD lands,

COGs and gradients in my hands,
Legends shimmer, peaks in sight,
Hover, pan — the heights delight.
Tiny rabbit, big map: alright! 🗻✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main change: adding AHN4 elevation map feature with COG integration, which is the primary objective across all modified files.
Linked Issues check ✅ Passed The PR implements all core acceptance criteria: AHN4 DTM displayed as raster layer on MapLibre, sourced from PDOK COG service, color-coded legend with dynamic min/max scaling based on viewport, performance optimizations via debouncing, and graceful error handling.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #361 objectives. Updates to sidebar (apps.tsx), header (atlas.tsx), and controls (atlas-controls.tsx) support the new elevation feature by enabling navigation and UI integration.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx Dismissed
@coderabbitai coderabbitai Bot changed the title @coderabbitai Add AHN4 elevation map feature with COG integration Dec 15, 2025
@coderabbitai coderabbitai Bot added branch:development Issue only affecting development, not the main branch (yet) dependencies Pull requests that update a dependency file enhancement New feature or request fdm-app labels Dec 15, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
fdm-app/package.json (1)

17-47: Consider pinning COG protocol dependency more strictly and reviewing Turf duplication

Two small dependency hygiene points:

  • @geomatico/maplibre-cog-protocol is pulled from a branch (#add-prepare). For reproducibility, consider pinning to a specific commit SHA or a released version once available.
  • You now depend on both @turf/turf and @turf/boolean-intersects. If you don’t need the standalone package for bundle-size reasons, you could drop it and import from @turf/turf instead to reduce duplication.
fdm-app/app/components/blocks/header/atlas.tsx (1)

2-51: Header dropdown implementation looks solid; consider tightening the route check

The dropdown wiring and use of NavLink for “Gewaspercelen” / “Hoogtekaart” look good and align with the new atlas routes.

You currently derive isElevation via location.pathname.includes("/elevation"). If this header is ever reused on other pages that also contain /elevation in the path, this could mislabel the current view. A more specific check (e.g. matching the atlas elevation route prefix or equality) would make this a bit more robust, but it’s not blocking.

fdm-app/app/components/blocks/atlas/atlas-controls.tsx (1)

1-195: Nice abstraction of map controls; small DRY opportunity

The refactor to a generic ControlButton + CustomControl works well, and the new ElevationControl is wired cleanly via showElevation / onToggleElevation.

Both FieldsControl and ElevationControl repeat the same CustomControl construction and updateProps shape (only labels and icon differ). You could factor a small helper like createToggleControlProps({ active, onToggle, labels, Icon }) to avoid duplicating these object literals in two places, but the current version is perfectly readable.

fdm-app/app/components/blocks/sidebar/apps.tsx (1)

43-58: Atlas sidebar collapsible and sub-links look consistent with existing patterns

The introduction of atlasFieldsLink / atlasElevationLink and the collapsible “Atlas” section fits nicely with the existing sidebar structure (e.g. Balans). Conditional enabling in the create-farm wizard and the active-state wiring via isActive all look correct.

If you ever add routes whose paths merely contain these URLs as substrings, the location.pathname.includes(atlasFieldsLink/atlasElevationLink) checks could become slightly over-broad. In that case, switching to stricter matching (e.g. equality or prefix checks) would avoid accidental active states, but for the current route set this is fine.

Also applies to: 100-158

fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx (2)

1-90: Projection and COG protocol setup are appropriate; consider using Turf for intersections

Registering EPSG:28992 via proj4.defs and the global maplibregl.addProtocol("cog", cogProtocol) at module scope is a reasonable one-time setup for this route.

For tile visibility, you’ve implemented isPointInPolygon + polygonIntersectsPolygon with a simplified “any vertex inside the other” heuristic. Given that kaartbladindex polygons are regular tiles, this is likely sufficient in practice, but you’ve also introduced @turf/boolean-intersects as a dependency in package.json. If you want more robust intersection handling with less custom geometry code to maintain, you could delegate this check to Turf instead of keeping a bespoke implementation.


266-436: Viewport sampling and COG tile updates are thoughtful; watch network load per update

The updateVisibleTiles flow is well-designed overall: zoom gating (<13 uses WMS, >=13 uses COG), cancellation via updateId, slow-network detection, legend range padding, and limiting to 24 tiles all help keep things responsive.

One thing to keep an eye on is the number of remote reads per update: for a 4×4 grid you can end up calling locationValues once per sample point, even when several samples land on the same tile. In practice that means up to 16 COG queries per throttled update. If you see this becoming a bottleneck, a small optimization would be to:

  • Group samplePoints by the tile feature they fall into, and
  • Call locationValues once per (tile, samplePoints[]) pair, or cache per-tile results across nearby updates.

Not urgent, but it could materially reduce network and COG decoding load on very active panning/zooming sessions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c94bf3 and 17584b5.

⛔ Files ignored due to path filters (2)
  • fdm-app/app/lib/cache.server.ts is excluded by !fdm-app/app/lib/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • .changeset/jolly-ravens-shop.md (1 hunks)
  • fdm-app/app/components/blocks/atlas/atlas-controls.tsx (8 hunks)
  • fdm-app/app/components/blocks/atlas/atlas-legend.tsx (1 hunks)
  • fdm-app/app/components/blocks/header/atlas.tsx (2 hunks)
  • fdm-app/app/components/blocks/sidebar/apps.tsx (2 hunks)
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx (2 hunks)
  • fdm-app/package.json (2 hunks)
  • fdm-app/vite.config.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (17)
📚 Learning: 2024-11-25T12:42:32.783Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 6
File: fdm-app/vite.config.ts:5-9
Timestamp: 2024-11-25T12:42:32.783Z
Learning: In the `fdm-app` project, SvenVw is preparing for migration to Remix v3 and may include type declarations or configurations for v3 features in advance, such as in `vite.config.ts`.

Applied to files:

  • fdm-app/vite.config.ts
📚 Learning: 2024-12-11T12:09:35.540Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 20
File: fdm-app/tsconfig.json:8-9
Timestamp: 2024-12-11T12:09:35.540Z
Learning: In the `fdm-app/tsconfig.json` file, the include path `.react-router/types/**/*` refers to a build-time generated directory which is intentionally not included in the repository.

Applied to files:

  • fdm-app/vite.config.ts
📚 Learning: 2025-01-24T11:46:49.990Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 49
File: fdm-data/rollup.config.js:7-17
Timestamp: 2025-01-24T11:46:49.990Z
Learning: When suggesting external dependencies in Rollup configuration, only include packages that are actually listed in the package's dependencies or peerDependencies.

Applied to files:

  • fdm-app/vite.config.ts
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 42
File: fdm-app/app/routes/farm/_b_id_farm/layout.tsx:46-95
Timestamp: 2025-01-09T16:03:37.764Z
Learning: A shared layout component `FarmLayoutBase` has been created in `components/custom/farm-layout-base.tsx` to maintain consistency across farm-related pages. The component handles farm selection dropdown, breadcrumb navigation, and provides a common layout structure.

Applied to files:

  • fdm-app/app/components/blocks/sidebar/apps.tsx
  • fdm-app/app/components/blocks/header/atlas.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 42
File: fdm-app/app/routes/farm/_b_id_farm/layout.tsx:46-95
Timestamp: 2025-01-09T16:03:37.764Z
Learning: The farm layout system has been reorganized into separate components (`FarmHeader`, `ContentLayout`, `PaginationLayout`) to support different navigation patterns (sidebar, pagination) while maintaining consistent styling. Each layout component is designed to be used independently or combined as needed.

Applied to files:

  • fdm-app/app/components/blocks/sidebar/apps.tsx
📚 Learning: 2025-09-23T12:29:34.184Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 274
File: fdm-app/app/routes/farm.$b_id_farm._index.tsx:160-163
Timestamp: 2025-09-23T12:29:34.184Z
Learning: In the FDM application, the fertilizer application route intentionally uses `${calendar}/field/fertilizer` instead of the originally planned `/farm/{farmId}/add/fertilizer` structure. This design decision prioritizes starting from the field list view to provide better field selection workflow before applying fertilizer, rather than direct dashboard-to-action navigation.

Applied to files:

  • fdm-app/app/components/blocks/sidebar/apps.tsx
📚 Learning: 2025-09-26T08:34:50.413Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 279
File: fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx:277-283
Timestamp: 2025-09-26T08:34:50.413Z
Learning: In the fdm project, fdm-core and fdm-app are updated together as part of a monorepo structure, which eliminates legacy data concerns when new fields like b_isproductive are introduced. Both packages are synchronized, so there's no need for defensive coding against undefined values for newly introduced database fields.

Applied to files:

  • fdm-app/package.json
📚 Learning: 2025-08-13T10:33:05.313Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 0
File: :0-0
Timestamp: 2025-08-13T10:33:05.313Z
Learning: In the fdm project, fdm-calculator integration for new features like b_lu_variety is handled in separate updates from the core data model changes. When fdm-core functions are updated to support new fields, fdm-calculator can consume these enhanced APIs without requiring changes in the same PR that introduces the core functionality.

Applied to files:

  • fdm-app/package.json
📚 Learning: 2025-06-10T13:10:03.154Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 161
File: fdm-app/app/components/blocks/field-map.tsx:0-0
Timestamp: 2025-06-10T13:10:03.154Z
Learning: When facing prop name inconsistencies with react-map-gl (like mapboxAccessToken vs mapboxApiAccessToken), using different import statements can resolve the issue more elegantly than changing prop names across multiple files.

Applied to files:

  • fdm-app/app/components/blocks/atlas/atlas-controls.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2024-11-25T14:42:26.660Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 6
File: fdm-app/app/components/blocks/field-map.tsx:0-0
Timestamp: 2024-11-25T14:42:26.660Z
Learning: In `fdm-app/app/components/blocks/field-map.tsx`, explicit cleanup of Mapbox GL resources is not necessary, as `react-map-gl` handles it automatically upon component unmount, and `MapRef` does not have a `remove` method.

Applied to files:

  • fdm-app/app/components/blocks/atlas/atlas-controls.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-31T15:41:43.741Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 67
File: fdm-app/app/components/custom/atlas/atlas-panels.tsx:28-28
Timestamp: 2025-01-31T15:41:43.741Z
Learning: When handling different map event types in react-map-gl v7.1.8, use MapLayerMouseEvent for mouse events (which have the point property) and ViewStateChangeEvent for view state changes. Use a type guard like 'point' in evt to safely access event-specific properties.

Applied to files:

  • fdm-app/app/components/blocks/atlas/atlas-controls.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-31T15:06:35.764Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 67
File: fdm-app/app/components/custom/atlas/atlas-sources.tsx:21-66
Timestamp: 2025-01-31T15:06:35.764Z
Learning: In react-map-gl components, when querying rendered features via map.queryRenderedFeatures(), the effect's dependency array must include any props that affect the map's rendered state (like source data) to ensure features are queried against the current map state.

Applied to files:

  • fdm-app/app/components/blocks/atlas/atlas-controls.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-31T16:06:33.810Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 67
File: fdm-app/app/routes/farm.create.$b_id_farm.atlas.tsx:164-212
Timestamp: 2025-01-31T16:06:33.810Z
Learning: Map configuration in the application should be modularized using the `useMapConfig` hook and `MapControls` component to maintain consistency across all MapGL instances.

Applied to files:

  • fdm-app/app/components/blocks/atlas/atlas-controls.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-31T16:06:33.810Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 67
File: fdm-app/app/routes/farm.create.$b_id_farm.atlas.tsx:164-212
Timestamp: 2025-01-31T16:06:33.810Z
Learning: MapGL implementations should use the shared `useMapConfig` hook for configuration and `getLayerStyle` utility for consistent styling. The hook supports both interactive and non-interactive maps, handling bounds calculation and view state management.

Applied to files:

  • fdm-app/app/components/blocks/atlas/atlas-controls.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 42
File: fdm-app/app/routes/farm/_b_id_farm/layout.tsx:46-95
Timestamp: 2025-01-09T16:03:37.764Z
Learning: The `FarmLayout` component in `components/custom/farm-layout.tsx` provides a reusable layout structure for farm-related pages, with support for farm selection dropdown, customizable breadcrumb titles, and flexible content rendering through either children or Outlet components.

Applied to files:

  • fdm-app/app/components/blocks/header/atlas.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2024-12-16T10:56:07.561Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 16
File: fdm-app/app/routes/app.addfarm.$b_id_farm.cultivations.$b_lu_catalogue.fertilizers.tsx:1-1
Timestamp: 2024-12-16T10:56:07.561Z
Learning: The project uses `react-router` v7, and the `data` function is exported and used for error handling in loaders and actions.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-31T14:29:37.599Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 67
File: fdm-app/app/components/custom/atlas/atlas.d.tsx:8-8
Timestamp: 2025-01-31T14:29:37.599Z
Learning: In the Atlas component's MapFieldsProps interface, mapStyle is intentionally restricted to "mapbox://styles/mapbox/satellite-streets-v12" as it's currently the only supported style option.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
🪛 GitHub Check: CodeQL
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx

[failure] 212-212: Clear text storage of sensitive information
This stores sensitive data returned by an access to longitude as clear text.
This stores sensitive data returned by an access to latitude as clear text.
This stores sensitive data returned by an access to longitude as clear text.
This stores sensitive data returned by an access to latitude as clear text.
This stores sensitive data returned by an access to longitude as clear text.
This stores sensitive data returned by an access to latitude as clear text.
This stores sensitive data returned by an access to longitude as clear text.
This stores sensitive data returned by an access to latitude as clear text.

🔇 Additional comments (6)
fdm-app/vite.config.ts (1)

34-35: Add COG protocol package to noExternal looks correct

Including @geomatico/maplibre-cog-protocol alongside the other SSR externals is consistent with the existing pattern and should help avoid SSR bundling issues. No further changes needed here.

.changeset/jolly-ravens-shop.md (1)

1-5: Changeset text and bump level match the feature scope

Description clearly captures the AHN4 elevation work, and using a minor bump for @svenvw/fdm-app is appropriate.

fdm-app/app/components/blocks/atlas/atlas-legend.tsx (1)

1-63: ElevationLegend component is well-scoped and readable

Props, conditional rendering (loading/slow/error), and the gradient scale are all handled cleanly. The min/max/hover formatting and fallbacks (“Laag”/“Hoog”) make sense for the AHN4 use case. No changes needed from my side.

fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx (3)

110-150: Loader logic for fields and map style looks correct

The loader’s guard on b_id_farm && b_id_farm !== "undefined" and conversion of getFields results into a GeoJSON FeatureCollection (with the extra b_lu_name / b_id_source properties) are all consistent and type-safe. Returning mapStyle = getMapStyle("satellite") alongside fields/calendar provides everything the client component needs without over-fetching. No changes needed here.


468-521: Hover elevation handler and throttling look good

The throttled handleMouseMove that:

  • Short-circuits below zoom 13 or when no active tiles,
  • Transforms to RD and reuses the same point-in-polygon logic as the tile sampler, and
  • Calls locationValues for the hovered location and updates hoverElevation,

is a sensible approach and ties in nicely with ElevationLegend. Using a shared stateRef to avoid stale closures in the throttled function is also a nice touch. I don’t see any correctness issues here.


524-662: MapGL composition and layering are coherent and match the intended UX

The main MapGL instance is wired cleanly:

  • View state is controlled via onMove / onMoveEnd with throttled tile updates.
  • showElevation consistently gates onMouseMove, WMS overview, COG raster tiles, hillshade, and the legend’s showScale.
  • Fields are overlaid via a single GeoJSON source with outline and (invisible but interactive) fill layers, with beforeId correctly positioning elevation layers under the outlines when fields are present.
  • The floating ElevationLegend + FieldsPanelHover panel in the top-left corner matches the Atlas UX.

Overall this composition looks robust and matches the AHN4 elevation feature description.

Comment thread fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 15, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.41%. Comparing base (3c94bf3) to head (d0f243c).
⚠️ Report is 16 commits behind head on development.

Additional details and impacted files
@@             Coverage Diff              @@
##           development     #376   +/-   ##
============================================
  Coverage        87.41%   87.41%           
============================================
  Files               91       91           
  Lines             4497     4497           
  Branches          1345     1345           
============================================
  Hits              3931     3931           
  Misses             566      566           
Flag Coverage Δ
fdm-calculator 87.36% <ø> (ø)
fdm-core 87.08% <ø> (ø)
fdm-data 92.12% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@SvenVw SvenVw requested a review from BoraIneviNMI December 15, 2025 12:28
Copy link
Copy Markdown
Collaborator

@BoraIneviNMI BoraIneviNMI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks and works great.

I was only annoyed as an user by all tiles disappearing while the map switches from WMS to COG or the other way, and when my connection is slow. Seconds would pass before new elevation detail appeared.

I tried to find a fix myself but couldn't. I thought of using the network state to clear the previous tiles at the right time, but network state wasn't very useful for this.

Maybe it is just my connection and this is not an important concern.

@SvenVw
Copy link
Copy Markdown
Collaborator Author

SvenVw commented Dec 17, 2025

It looks and works great.

I was only annoyed as an user by all tiles disappearing while the map switches from WMS to COG or the other way, and when my connection is slow. Seconds would pass before new elevation detail appeared.

I tried to find a fix myself but couldn't. I thought of using the network state to clear the previous tiles at the right time, but network state wasn't very useful for this.

Maybe it is just my connection and this is not an important concern.

Thanks for the feedback! I have improved the transition between WMS and COG in ba8ef5f

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx (4)

267-436: Consider extracting tile update logic into smaller functions.

The updateVisibleTiles function handles multiple responsibilities in 170 lines: viewport conversion, tile filtering, elevation sampling, and min/max calculation. This makes the code harder to test and maintain.

Consider extracting helper functions:

// Separate concerns into focused functions
function convertViewportToRD(bounds: LngLatBounds): [number, number][] {
    // Lines 292-305
}

function findIntersectingTiles(
    viewportRD: [number, number][],
    index: FeatureCollection,
    maxTiles: number = 24
): Feature[] {
    // Lines 309-316
}

async function sampleElevationRange(
    bounds: LngLatBounds,
    visibleFeatures: Feature[],
    gridSize: number = 3
): Promise<{ min: number; max: number }> {
    // Lines 319-396
}

function createActiveTiles(
    features: Feature[],
    min: number,
    max: number
): ActiveTile[] {
    // Lines 404-421
}

This would make the main function more readable and each piece independently testable.


316-316: Document the 24-tile limit rationale.

The hard-coded limit .slice(0, 24) constrains visible tiles but lacks explanation for why 24 specifically. Consider adding a comment explaining the performance/memory trade-off, or extracting it as a named constant.

+// Limit active tiles to prevent memory/performance issues
+const MAX_ACTIVE_TILES = 24
+
 const visibleFeatures = indexData.features
     .filter((f) => {
         if (!f.geometry || f.geometry.type !== "Polygon")
             return false
         const ring = (f.geometry as any).coordinates[0]
         return polygonIntersectsPolygon(rdCoords, ring)
     })
-    .slice(0, 24)
+    .slice(0, MAX_ACTIVE_TILES)

313-313: Improve type safety for GeoJSON geometry access.

The code uses as any to access polygon coordinates in multiple places (lines 313, 344, 489). This bypasses TypeScript's type checking and could lead to runtime errors if the geometry type assumption is incorrect.

Create a type guard:

import type { Polygon } from "geojson"

function isPolygonFeature(feature: Feature): feature is Feature<Polygon> {
    return feature.geometry?.type === "Polygon"
}

Then use it consistently:

 const visibleFeatures = indexData.features
     .filter((f) => {
-        if (!f.geometry || f.geometry.type !== "Polygon")
-            return false
-        const ring = (f.geometry as any).coordinates[0]
-        return polygonIntersectsPolygon(rdCoords, ring)
+        if (!isPolygonFeature(f)) return false
+        const ring = f.geometry.coordinates[0]
+        return polygonIntersectsPolygon(rdCoords, ring)
     })

Apply the same pattern at lines 344 and 489.


581-581: Consider extracting Netherlands bounds as a named constant.

The bounds [3.3, 50.7, 7.2, 53.7] appear multiple times (lines 581, 602) and represent the Netherlands bounding box. Extracting this as a named constant would improve maintainability.

const NETHERLANDS_BOUNDS: [number, number, number, number] = [3.3, 50.7, 7.2, 53.7]

// Then use in Source components:
<Source
    // ...
    bounds={NETHERLANDS_BOUNDS}
/>
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17584b5 and ba8ef5f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • fdm-app/app/components/blocks/atlas/atlas-legend.tsx (1 hunks)
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx (2 hunks)
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx (1 hunks)
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • fdm-app/app/components/blocks/atlas/atlas-legend.tsx
🧰 Additional context used
🧠 Learnings (15)
📚 Learning: 2025-09-23T12:27:07.391Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 274
File: fdm-app/app/routes/farm.$b_id_farm._index.tsx:151-204
Timestamp: 2025-09-23T12:27:07.391Z
Learning: In the FDM application, field overview functionality is implemented as a dedicated page accessible via `farm/{farmId}/{calendar}/field` rather than as a direct listing on the dashboard. The dashboard includes a "Perceelsoverzicht" quick action card that provides navigation to this comprehensive field management interface.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 42
File: fdm-app/app/routes/farm/_b_id_farm/layout.tsx:46-95
Timestamp: 2025-01-09T16:03:37.764Z
Learning: A shared layout component `FarmLayoutBase` has been created in `components/custom/farm-layout-base.tsx` to maintain consistency across farm-related pages. The component handles farm selection dropdown, breadcrumb navigation, and provides a common layout structure.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx
📚 Learning: 2025-12-15T12:19:47.858Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 376
File: fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx:187-213
Timestamp: 2025-12-15T12:19:47.858Z
Learning: When reviewing code, prefer storing only non-sensitive UI/state data in sessionStorage. For map viewState (e.g., longitude/latitude), ensure it represents non-personal business data and that persistence across sessions is justified, documented, and respects user privacy. If persisting, use a clearly scoped, namespaced key, guard access with try/catch, and avoid syncing with servers or exposing data to third-party scripts. Apply this guideline to all TSX files that manage client-side UI state.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-09-23T12:29:34.184Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 274
File: fdm-app/app/routes/farm.$b_id_farm._index.tsx:160-163
Timestamp: 2025-09-23T12:29:34.184Z
Learning: In the FDM application, the fertilizer application route intentionally uses `${calendar}/field/fertilizer` instead of the originally planned `/farm/{farmId}/add/fertilizer` structure. This design decision prioritizes starting from the field list view to provide better field selection workflow before applying fertilizer, rather than direct dashboard-to-action navigation.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx
📚 Learning: 2025-08-11T12:24:32.200Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 233
File: fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx:53-53
Timestamp: 2025-08-11T12:24:32.200Z
Learning: In `fdm-app/app/components/blocks/atlas-fields/cultivation-history.tsx`, the NMI API for cultivations guarantees that each year will be unique in the cultivation history data, so using `cultivation.year` as a React list key is safe and won't cause duplicate key warnings.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 42
File: fdm-app/app/routes/farm/_b_id_farm/layout.tsx:46-95
Timestamp: 2025-01-09T16:03:37.764Z
Learning: The `FarmLayout` component in `components/custom/farm-layout.tsx` provides a reusable layout structure for farm-related pages, with support for farm selection dropdown, customizable breadcrumb titles, and flexible content rendering through either children or Outlet components.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx
📚 Learning: 2025-09-25T15:10:59.708Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 272
File: fdm-app/app/store/field-fertilizer-form.tsx:45-47
Timestamp: 2025-09-25T15:10:59.708Z
Learning: In the FDM application, Zustand stores with persist middleware using sessionStorage/localStorage don't require SSR hardening guards. The existing store patterns in fdm-app work without typeof window checks or memory storage fallbacks.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-09-25T15:10:59.708Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 272
File: fdm-app/app/store/field-fertilizer-form.tsx:45-47
Timestamp: 2025-09-25T15:10:59.708Z
Learning: In the FDM application, Zustand stores with persist middleware using sessionStorage/localStorage don't require SSR hardening guards. The existing store patterns in fdm-app work without typeof window checks or memory storage fallbacks, as evidenced by the changelog store using createJSONStorage(() => localStorage) directly.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2024-11-25T14:42:26.660Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 6
File: fdm-app/app/components/blocks/field-map.tsx:0-0
Timestamp: 2024-11-25T14:42:26.660Z
Learning: In `fdm-app/app/components/blocks/field-map.tsx`, explicit cleanup of Mapbox GL resources is not necessary, as `react-map-gl` handles it automatically upon component unmount, and `MapRef` does not have a `remove` method.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-31T16:06:33.810Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 67
File: fdm-app/app/routes/farm.create.$b_id_farm.atlas.tsx:164-212
Timestamp: 2025-01-31T16:06:33.810Z
Learning: Map configuration in the application should be modularized using the `useMapConfig` hook and `MapControls` component to maintain consistency across all MapGL instances.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-31T16:06:33.810Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 67
File: fdm-app/app/routes/farm.create.$b_id_farm.atlas.tsx:164-212
Timestamp: 2025-01-31T16:06:33.810Z
Learning: MapGL implementations should use the shared `useMapConfig` hook for configuration and `getLayerStyle` utility for consistent styling. The hook supports both interactive and non-interactive maps, handling bounds calculation and view state management.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-31T15:06:35.764Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 67
File: fdm-app/app/components/custom/atlas/atlas-sources.tsx:21-66
Timestamp: 2025-01-31T15:06:35.764Z
Learning: In react-map-gl components, when querying rendered features via map.queryRenderedFeatures(), the effect's dependency array must include any props that affect the map's rendered state (like source data) to ensure features are queried against the current map state.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-31T15:41:43.741Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 67
File: fdm-app/app/components/custom/atlas/atlas-panels.tsx:28-28
Timestamp: 2025-01-31T15:41:43.741Z
Learning: When handling different map event types in react-map-gl v7.1.8, use MapLayerMouseEvent for mouse events (which have the point property) and ViewStateChangeEvent for view state changes. Use a type guard like 'point' in evt to safely access event-specific properties.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-01-31T14:29:37.599Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 67
File: fdm-app/app/components/custom/atlas/atlas.d.tsx:8-8
Timestamp: 2025-01-31T14:29:37.599Z
Learning: In the Atlas component's MapFieldsProps interface, mapStyle is intentionally restricted to "mapbox://styles/mapbox/satellite-streets-v12" as it's currently the only supported style option.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
📚 Learning: 2025-06-10T13:10:03.154Z
Learnt from: SvenVw
Repo: SvenVw/fdm PR: 161
File: fdm-app/app/components/blocks/field-map.tsx:0-0
Timestamp: 2025-06-10T13:10:03.154Z
Learning: When facing prop name inconsistencies with react-map-gl (like mapboxAccessToken vs mapboxApiAccessToken), using different import statements can resolve the issue more elegantly than changing prop names across multiple files.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
🧬 Code graph analysis (1)
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx (5)
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx (1)
  • loader (42-77)
fdm-app/app/components/blocks/atlas/atlas-styles.tsx (1)
  • getFieldsStyle (8-12)
fdm-app/app/components/blocks/atlas/atlas-viewstate.tsx (1)
  • getViewState (19-42)
fdm-app/app/components/blocks/atlas/atlas-controls.tsx (1)
  • Controls (26-54)
fdm-app/app/components/blocks/atlas/atlas.tsx (1)
  • ZOOM_LEVEL_FIELDS (1-1)
🔇 Additional comments (7)
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.tsx (1)

24-24: Meta title update aligns with Atlas rebranding.

The title change from "Kaarten" to "Atlas" is consistent with the PR's objective to rename the map section.

fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.fields._index.tsx (1)

31-31: Consistent Atlas naming convention.

The meta title update maintains consistency with the broader Atlas rebranding across all map-related routes.

fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx (5)

54-83: Spatial intersection helpers are appropriate for RD coordinates.

The custom isPointInPolygon and polygonIntersectsPolygon implementations work directly with RD New (EPSG:28992) projected coordinates. While @turf/boolean-intersects is available as a dependency, it operates on WGS84 geographic coordinates, making these custom helpers necessary for the projected coordinate system.

The simplified intersection check is appropriate for tile filtering and should perform well.


106-154: Loader implementation follows established patterns.

The loader correctly fetches farm fields and prepares them as a GeoJSON FeatureCollection, consistent with the fields index route. Error handling and data transformation are appropriate.


161-183: State management is well-structured.

The component maintains appropriate state for elevation rendering, including tile tracking, loading states, legend ranges, and UI toggles. The separation of concerns between network status, update status, and data state is clear.


468-521: Hover elevation query is appropriately throttled.

The mouse move handler queries elevation values from COG tiles with 200ms throttling and proper guards for zoom level. The COG protocol should cache tile data, making repeated queries efficient. Error handling is acceptable for hover interactions.


552-623: Rendering strategy appropriately switches between WMS and COG based on zoom.

The implementation correctly uses WMS tiles for overview (zoom < 13) and switches to detailed COG tiles with hillshade at higher zoom levels. Layer ordering with beforeId ensures fields render above elevation data. The use of Fragment with tile.id as key is appropriate for dynamic tile lists.

Comment thread fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
Comment thread fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.elevation.tsx
@BoraIneviNMI
Copy link
Copy Markdown
Collaborator

It looks and works great.
I was only annoyed as an user by all tiles disappearing while the map switches from WMS to COG or the other way, and when my connection is slow. Seconds would pass before new elevation detail appeared.
I tried to find a fix myself but couldn't. I thought of using the network state to clear the previous tiles at the right time, but network state wasn't very useful for this.
Maybe it is just my connection and this is not an important concern.

Thanks for the feedback! I have improved the transition between WMS and COG in ba8ef5f

@SvenVw yes, seems to appear much more performant now. Great!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

branch:development Issue only affecting development, not the main branch (yet) dependencies Pull requests that update a dependency file enhancement New feature or request fdm-app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Dynamic AHN4 DTM Elevation Map with Cloud Optimized Geotiffs (COG)

3 participants