Skip to content

Show soil descriptions on the soil atlas#436

Merged
SvenVw merged 12 commits into
release/2026-01from
FDM404-soil-description
Jan 29, 2026
Merged

Show soil descriptions on the soil atlas#436
SvenVw merged 12 commits into
release/2026-01from
FDM404-soil-description

Conversation

@BoraIneviNMI
Copy link
Copy Markdown
Collaborator

@BoraIneviNMI BoraIneviNMI commented Jan 28, 2026

Enhancements

  • Soil descriptions are retrieved from Bodemdata.nl. They are also cached client-side.

Summary by CodeRabbit

Release Notes

  • New Features
    • Soil map popups now display detailed soil descriptions when clicked
    • Implemented intelligent caching to improve performance and reduce data fetching
    • Added loading indicator for visual feedback while soil data is being retrieved

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

@BoraIneviNMI BoraIneviNMI self-assigned this Jan 28, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 28, 2026

⚠️ No Changeset found

Latest commit: 5de0b34

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 28, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • development
  • main

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review

Walkthrough

This PR adds Bodemdata soil legend integration to the farm atlas feature. A new route loader fetches soil class data from an external Bodemdata API, while client-side code implements MRU caching, error handling, and UI rendering of fetched legend information in soil popups.

Changes

Cohort / File(s) Summary
Route Loader
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.soil.bodemdata.$soilcode.ts
New route handler that fetches Bodemdata soil class legends from external soilmaplegendserver using soilcode parameter.
Soil Atlas UI Enhancement
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.soil.tsx
Added BodemData types, MRU cache (20-item limit), fetchBodemData hook with caching and HTML cleanup, integrated data fetching into soil click handler, and enhanced popup UI to display bodemdata.omschrijving with loading state.

Sequence Diagram

sequenceDiagram
    participant User
    participant Client as Client (Atlas UI)
    participant RouteLoader as Route Loader
    participant BodemAPI as Bodemdata API

    User->>Client: Click on soil location
    Client->>Client: Check MRU cache for soilcode
    alt Cache hit
        Client->>Client: Retrieve cached bodemdata
    else Cache miss
        Client->>RouteLoader: Fetch /bodemdata/{soilcode}
        RouteLoader->>BodemAPI: Fetch soil legend
        BodemAPI-->>RouteLoader: Return legend data
        RouteLoader-->>Client: Return parsed response
        Client->>Client: Clean HTML tags & cache result
    end
    Client->>Client: Merge bodemdata into popup.properties
    Client->>User: Render popup with bodemdata.omschrijving
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement, fdm-app

Suggested reviewers

  • gerardhros

Poem

🐰 A rabbit hops through soil so deep,
With legends fetched in cache to keep,
MRU secrets, stored with care,
Bodemdata blooms in popups fair! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Show soil descriptions on the soil atlas' directly corresponds to the main objective of the PR: adding soil descriptions from Bodemdata.nl to the soil atlas UI. It accurately summarizes the primary change.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch FDM404-soil-description

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.07%. Comparing base (aa13654) to head (5de0b34).
⚠️ Report is 13 commits behind head on release/2026-01.

Additional details and impacted files
@@               Coverage Diff                @@
##           release/2026-01     #436   +/-   ##
================================================
  Coverage            88.07%   88.07%           
================================================
  Files                   91       91           
  Lines                 4621     4621           
  Branches              1492     1492           
================================================
  Hits                  4070     4070           
  Misses                 551      551           
Flag Coverage Δ
fdm-calculator 88.76% <ø> (ø)
fdm-core 86.77% <ø> (ø)
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.

@BoraIneviNMI
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added enhancement New feature or request fdm-app labels Jan 29, 2026
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

🤖 Fix all issues with AI agents
In
`@fdm-app/app/routes/farm`.$b_id_farm.$calendar.atlas.soil.bodemdata.$soilcode.ts:
- Around line 3-9: Wrap the external fetch in the loader function (loader) with
a try/catch and a timeout via AbortController: create an AbortController, set a
short timeout (e.g. 5s) that calls controller.abort(), pass controller.signal to
fetch(`https://legenda-bodemkaart.bodemdata.nl/soilmaplegendserver/item/bodemklasse/${params.soilcode}`),
and clear the timeout on success; in the catch block handle both AbortError and
other errors and return a proper response using the data() helper (or throw a
Response) with an appropriate status and error message so calling code receives
a controlled error instead of an unhandled exception.

In `@fdm-app/app/routes/farm`.$b_id_farm.$calendar.atlas.soil.tsx:
- Around line 194-206: The closure computes cachedIndex from the outer
cachedBodemData but then mutates state in setCachedBodemData's functional
updater, causing race conditions; move the index computation inside the updater
by calling findIndex on the updater argument (e.g., current => { const idx =
current.findIndex(item => item.key === soilcode); if (idx > -1) { const update =
[...current]; const [cached] = update.splice(idx,1); update.push(cached); return
update; } return current; }) and obtain the found.value from current[idx] (or
the spliced cached) so you still return the correct value to the caller while
avoiding the stale-closure bug in setCachedBodemData.
🧹 Nitpick comments (2)
fdm-app/app/routes/farm.$b_id_farm.$calendar.atlas.soil.tsx (2)

208-210: Hardcoded undefined in the fetch URL.

The URL path /farm/undefined/all/atlas/soil/bodemdata/${soilcode} hardcodes "undefined" for the farm ID. While this works because the server-side loader doesn't use these route params, this is fragile and unclear to future maintainers. Consider using a dedicated API route path that doesn't require placeholder route segments.


216-218: HTML tag stripping is limited to <p> tags.

Consider using a more robust approach to strip HTML tags if the API response could contain other HTML elements.

♻️ Alternative: strip all HTML tags
-                    data.omschrijving = data.omschrijving
-                        ?.replaceAll("<p>", "")
-                        .replaceAll("</p>", "")
+                    data.omschrijving = data.omschrijving
+                        ?.replace(/<[^>]*>/g, "")

Comment on lines +3 to +9
export async function loader({ params }: Route.LoaderArgs) {
// Fetching client-side leads to CORS and CSP errors.
// CSP issues can be resolved but CORS issues can't be without contacting Bodemdata.
return fetch(
`https://legenda-bodemkaart.bodemdata.nl/soilmaplegendserver/item/bodemklasse/${params.soilcode}`,
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling and consider a timeout for the external fetch.

The loader returns the fetch result directly without error handling. If the external Bodemdata API is unavailable or returns an error, this will propagate as an unhandled exception. Additionally, there's no timeout, so a slow external service could hang requests indefinitely.

🛡️ Proposed fix with error handling and timeout
+import { data } from "react-router"
 import type { Route } from "./+types/farm.$b_id_farm.$calendar.atlas.soil.bodemdata.$soilcode"

 export async function loader({ params }: Route.LoaderArgs) {
     // Fetching client-side leads to CORS and CSP errors.
     // CSP issues can be resolved but CORS issues can't be without contacting Bodemdata.
-    return fetch(
-        `https://legenda-bodemkaart.bodemdata.nl/soilmaplegendserver/item/bodemklasse/${params.soilcode}`,
-    )
+    try {
+        const controller = new AbortController()
+        const timeoutId = setTimeout(() => controller.abort(), 5000)
+        
+        const response = await fetch(
+            `https://legenda-bodemkaart.bodemdata.nl/soilmaplegendserver/item/bodemklasse/${params.soilcode}`,
+            { signal: controller.signal }
+        )
+        clearTimeout(timeoutId)
+        
+        if (!response.ok) {
+            return data({ success: false, error: "External API error" }, { status: 502 })
+        }
+        
+        const json = await response.json()
+        return data({ success: true, data: json })
+    } catch (error) {
+        return data({ success: false, error: "Failed to fetch soil data" }, { status: 502 })
+    }
 }

Based on learnings: In React Router v7, the data() function is used for creating responses in loaders and actions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function loader({ params }: Route.LoaderArgs) {
// Fetching client-side leads to CORS and CSP errors.
// CSP issues can be resolved but CORS issues can't be without contacting Bodemdata.
return fetch(
`https://legenda-bodemkaart.bodemdata.nl/soilmaplegendserver/item/bodemklasse/${params.soilcode}`,
)
}
import { data } from "react-router"
import type { Route } from "./+types/farm.$b_id_farm.$calendar.atlas.soil.bodemdata.$soilcode"
export async function loader({ params }: Route.LoaderArgs) {
// Fetching client-side leads to CORS and CSP errors.
// CSP issues can be resolved but CORS issues can't be without contacting Bodemdata.
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000)
const response = await fetch(
`https://legenda-bodemkaart.bodemdata.nl/soilmaplegendserver/item/bodemklasse/${params.soilcode}`,
{ signal: controller.signal }
)
clearTimeout(timeoutId)
if (!response.ok) {
return data({ success: false, error: "External API error" }, { status: 502 })
}
const json = await response.json()
return data({ success: true, data: json })
} catch (error) {
return data({ success: false, error: "Failed to fetch soil data" }, { status: 502 })
}
}
🤖 Prompt for AI Agents
In
`@fdm-app/app/routes/farm`.$b_id_farm.$calendar.atlas.soil.bodemdata.$soilcode.ts
around lines 3 - 9, Wrap the external fetch in the loader function (loader) with
a try/catch and a timeout via AbortController: create an AbortController, set a
short timeout (e.g. 5s) that calls controller.abort(), pass controller.signal to
fetch(`https://legenda-bodemkaart.bodemdata.nl/soilmaplegendserver/item/bodemklasse/${params.soilcode}`),
and clear the timeout on success; in the catch block handle both AbortError and
other errors and return a proper response using the data() helper (or throw a
Response) with an appropriate status and error message so calling code receives
a controlled error instead of an unhandled exception.

Comment on lines +194 to +206
const cachedIndex = cachedBodemData.findIndex(
(item) => item.key === soilcode,
)
if (cachedIndex > -1) {
// If found in the cache, move the cached item to the end of the list
const found = cachedBodemData[cachedIndex]
setCachedBodemData((cachedBodemData) => {
const update = [...cachedBodemData]
const cached = update.splice(cachedIndex, 1)
update.push(cached[0])
return update
})
return found.value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Stale closure issue in cache update logic.

The cachedIndex is computed from the cachedBodemData closure value, but then used inside setCachedBodemData's functional update which receives the current state. If another cache update occurs between computing the index and executing the state update, the splice will operate on the wrong index.

🐛 Proposed fix: compute index inside functional update
             if (cachedIndex > -1) {
                 // If found in the cache, move the cached item to the end of the list
                 const found = cachedBodemData[cachedIndex]
-                setCachedBodemData((cachedBodemData) => {
-                    const update = [...cachedBodemData]
-                    const cached = update.splice(cachedIndex, 1)
-                    update.push(cached[0])
+                setCachedBodemData((current) => {
+                    const currentIndex = current.findIndex(
+                        (item) => item.key === soilcode,
+                    )
+                    if (currentIndex === -1) return current
+                    const update = [...current]
+                    const cached = update.splice(currentIndex, 1)
+                    update.push(cached[0])
                     return update
                 })
                 return found.value
             }
🤖 Prompt for AI Agents
In `@fdm-app/app/routes/farm`.$b_id_farm.$calendar.atlas.soil.tsx around lines 194
- 206, The closure computes cachedIndex from the outer cachedBodemData but then
mutates state in setCachedBodemData's functional updater, causing race
conditions; move the index computation inside the updater by calling findIndex
on the updater argument (e.g., current => { const idx = current.findIndex(item
=> item.key === soilcode); if (idx > -1) { const update = [...current]; const
[cached] = update.splice(idx,1); update.push(cached); return update; } return
current; }) and obtain the found.value from current[idx] (or the spliced cached)
so you still return the correct value to the caller while avoiding the
stale-closure bug in setCachedBodemData.

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

Finished now.

@BoraIneviNMI BoraIneviNMI requested a review from SvenVw January 29, 2026 10:21
@BoraIneviNMI
Copy link
Copy Markdown
Collaborator Author

Now the badge shows the first soil code too.

@SvenVw SvenVw merged commit 93d9f26 into release/2026-01 Jan 29, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request fdm-app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants