Skip to content

Per-field norms errors, GeoTIFF refactor, derogatievrije zones support#280

Merged
SvenVw merged 27 commits into
developmentfrom
FDM276
Sep 29, 2025
Merged

Per-field norms errors, GeoTIFF refactor, derogatievrije zones support#280
SvenVw merged 27 commits into
developmentfrom
FDM276

Conversation

@SvenVw
Copy link
Copy Markdown
Collaborator

@SvenVw SvenVw commented Sep 26, 2025

Summary by CodeRabbit

  • New Features

    • Per-field error indicators plus a page-level warning with tooltip; successful fields still render.
    • Support for derogatievrije zones and finer sub-type handling for cultivation norms.
    • Added reusable GeoTIFF utilities and a centralized public-data URL helper.
  • Bug Fixes

    • Prevented exceptions when computing certain stikstofgebruiksnormen (including cases with missing potato variety).
  • Refactor

    • Switched various geographic and deposition checks to GeoTIFF-based lookups and simplified raster access.
  • Tests

    • Updated and extended tests covering norms, derogation zones, and related logic.
  • Chore

    • Adjusted module resolution configuration.

Closes #276

@SvenVw SvenVw self-assigned this Sep 26, 2025
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Sep 26, 2025

🦋 Changeset detected

Latest commit: fb778ac

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

This PR includes changesets to release 2 packages
Name Type
@svenvw/fdm-app Patch
@svenvw/fdm-calculator 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 Sep 26, 2025

Walkthrough

Adds per-field norm error handling and UI indicators; centralizes public-data URL and GeoTIFF helpers; replaces vector lookups with GeoTIFF queries for multiple norm/area checks; restructures stikstof norm data to use per-sub-type entries; updates deposition logic, tests, and TypeScript module resolution.

Changes

Cohort / File(s) Summary
Changesets
.changeset/*
Adds multiple changeset files bumping @svenvw/fdm-app and @svenvw/fdm-calculator with notes about field-level error handling, sub-type handling, GeoTIFF lookups, potato variety restructuring, derogatievrije zones, and year-based stikstof norms.
Norms UI components
fdm-app/app/components/blocks/norms/farm-norms.tsx, fdm-app/app/components/blocks/norms/field-norms.tsx
FarmNorms gains hasFieldNormErrors and fieldErrorMessages props and renders an alert tooltip; FieldNorm is exported and may include errorMessage?; field rendering conditionally shows an error banner or norm blocks.
Norms route loader
fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx
Replaces bulk Promise.all with per-field try/catch and Promise.allSettled; collects per-field errorMessages into fieldErrorMessages and hasFieldNormErrors; filters invalid fields from farm aggregation; loader returns new UI fields.
GeoTIFF shared utilities
fdm-calculator/src/shared/geotiff.ts, fdm-calculator/src/shared/public-data-url.ts
New getTiff (deduped cache) and getGeoTiffValue(url, lon, lat) with OOB/NoData handling; new getFdmPublicDataUrl() helper returning FDM public base URL.
Public-data URL refactor
fdm-calculator/src/balance/nitrogen/index.ts, .../index.test.ts, .../supply/deposition.test.ts
Removed local getFdmPublicDataUrl implementation; import now from shared helper; removed redundant local URL test block and adjusted imports.
Deposition via GeoTIFF
fdm-calculator/src/balance/nitrogen/supply/deposition.ts
Replaces in-file GeoTIFF caching and windowed reads with getGeoTiffValue(url, lon, lat) per-field lookups; removes tiff caches/getTiff logic and image metadata handling from this file.
Dierlijke-mest norms & area checks
fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts, .../dierlijke-mest-gebruiksnorm.test.ts
Replaces vector queries with GeoTIFF lookups for GWBG/Natura2000; adds and exports isFieldInDerogatieVrijeZone; integrates derogatievrije-zone into norm selection; adds tests for zone detection.
Stikstof norms logic/data/types
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts, .../stikstofgebruiksnorm-data.ts, .../types.d.ts, .../stikstofgebruiksnorm.test.ts
NV/region checks moved to GeoTIFF via getGeoTiffValue; introduced determineSubTypeOmschrijving; getNormsForCultivation now accepts optional subTypeOmschrijving; data reorganized to sub_types with per-sub-type varieties and norms; top-level variety/norm fields removed; tests updated/expanded.
GeoTIFF test/import adjustments
fdm-calculator/src/balance/nitrogen/supply/deposition.test.ts, fdm-calculator/src/balance/nitrogen/index.test.ts
Tests/imports adjusted to use shared public-data-url; removed a local URL test suite.
Shared new file
fdm-calculator/src/shared/geotiff.ts
New module implementing TIFF caching and getGeoTiffValue pixel lookup; exported getTiff and getGeoTiffValue.
TypeScript config
fdm-calculator/tsconfig.json
Adds compilerOptions.baseUrl: "./src" to improve module resolution.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Route as Norms route (loader)
  participant Calc as Norms calculator
  participant Geo as GeoTIFF utils
  participant Storage as Public Data Storage

  Route->>Route: Build per-field promises (try/catch)
  par For each field
    Route->>Calc: request field norms
    Calc->>Geo: getGeoTiffValue(url, lon, lat) for NV/region/areas
    Geo->>Storage: fetch TIFF (deduped/cache)
    Storage-->>Geo: TIFF bytes
    Geo-->>Calc: raster value or null
    alt success
      Calc-->>Route: norms object
    else error
      Calc-->>Route: throws -> Route captures field.errorMessage
    end
  end
  Route->>Route: Promise.allSettled -> assemble fieldNorms[]
  Route->>Route: aggregate hasFieldNormErrors & fieldErrorMessages
  Route->>Route: compute farm totals from valid fields
  Route-->>UI: render FieldNorms (inline errors) and FarmNorms (tooltip)
Loading
sequenceDiagram
  autonumber
  participant C as getNL2025StikstofGebruiksNorm
  participant D as determineSubTypeOmschrijving
  participant G as getNormsForCultivation
  participant Geo as GeoTIFF utils

  C->>Geo: getGeoTiffValue(...grondsoortenkaart) -> region code
  Geo-->>C: region
  C->>Geo: getGeoTiffValue(...nv_gebied) -> nv flag
  Geo-->>C: nv flag
  C->>D: derive subTypeOmschrijving (cultivation/history/variety)
  D-->>C: subTypeOmschrijving?
  C->>G: getNormsForCultivation(standard, date, subTypeOmschrijving)
  G-->>C: NormsByRegion (selected sub_type/norms)
  C-->>Caller: composed normValue and normSource (may include sub-type text)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Suggested reviewers

  • BoraIneviNMI
  • gerardhros

Poem

"I hopped through TIFFs and tiny tiles,
I sniffed each field across the miles.
When one went wrong I flagged the spot,
Kept others shown—no loss, no blot.
Sub-types found, the norms align—hop, code, and smile!" 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning This pull request introduces numerous changes unrelated to sub-type handling—such as extensive UI error-handling updates in fdm-app, a new GeoTIFF utility module, deposition refactors, and tsconfig adjustments—that go beyond the scope of the linked issue’s requirements. Please split unrelated work into separate PRs, isolating the sub-type selection improvements to getNormsForCultivation in this PR and moving UI enhancements, GeoTIFF caching, deposition logic, and configuration changes to their own focused reviews.
Title Check ❓ Inconclusive The title highlights per-field error handling, GeoTIFF refactoring, and derogatievrije zone support, which are indeed part of the changes, but it omits the core objective of enhancing sub-type selection logic in getNormsForCultivation, making it only partially representative of the primary goal. Consider revising the title to explicitly reference the sub-type selection enhancements for getNormsForCultivation—such as “Add sub-type selection to getNormsForCultivation, per-field errors, and GeoTIFF refactor”—to more accurately capture the PR’s main purpose.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues Check ✅ Passed The PR fully implements the objectives of issue #276 by updating getNormsForCultivation to accept an optional subTypeOmschrijving, adding determineSubTypeOmschrijving logic in getNL2025StikstofGebruiksNorm to infer and pass the correct description based on cultivation code, history, and order, and by expanding test coverage to validate the new behavior.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch FDM276

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 Sep 26, 2025

Codecov Report

❌ Patch coverage is 90.95745% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.20%. Comparing base (438f597) to head (fb778ac).
⚠️ Report is 28 commits behind head on development.

Files with missing lines Patch % Lines
...lculator/src/norms/nl/2025/stikstofgebruiksnorm.ts 85.13% 22 Missing ⚠️
fdm-calculator/src/shared/geotiff.ts 88.13% 7 Missing ⚠️
...r/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts 91.93% 5 Missing ⚠️
Additional details and impacted files
@@               Coverage Diff               @@
##           development     #280      +/-   ##
===============================================
+ Coverage        92.93%   93.20%   +0.26%     
===============================================
  Files               79       81       +2     
  Lines            12952    12901      -51     
  Branches          1268     1305      +37     
===============================================
- Hits             12037    12024      -13     
+ Misses             913      875      -38     
  Partials             2        2              
Flag Coverage Δ
fdm-calculator 95.05% <90.95%> (+0.69%) ⬆️
fdm-core 91.92% <ø> (ø)
fdm-data 94.40% <ø> (ø)

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.

@coderabbitai coderabbitai Bot changed the title @coderabbitai Per-field norms errors, GeoTIFF refactor, derogatievrije zones support Sep 26, 2025
@coderabbitai coderabbitai Bot added enhancement New feature or request fdm-calculator labels Sep 26, 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: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx (1)

127-171: Fix undefined variable: fieldOptions is not defined in loader

The catch block uses fieldOptions, but it’s only defined later in the client component, not in the loader scope. This will throw at runtime.

Apply this diff:

-                        const fieldName =
-                            fieldOptions.find((opt) => opt.b_id === field.b_id)
-                                ?.b_name || `Perceel ${field.b_id}`
+                        const fieldName =
+                            fields.find((f) => f.b_id === field.b_id)?.b_name ||
+                            `Perceel ${field.b_id}`
🧹 Nitpick comments (24)
.changeset/kind-singers-arrive.md (1)

5-5: Polish the changeset description for clarity.

Suggest rewording and punctuation for readability.

-At norms replace vector lookup of remote datasets with raster query to improve performance and reliability
+In norms, replace vector lookups of remote datasets with raster queries to improve performance and reliability.
.changeset/tame-cars-kneel.md (1)

5-5: Minor wording improvement.

Small grammar tweak for clarity.

-Add support for cultivation with different stikstofgebruiksnormen for 1st and later years
+Add support for cultivations with different stikstofgebruiksnormen for first and subsequent years.
.changeset/every-shoes-boil.md (1)

5-5: Fix grammar.

Use “when calculating” instead of “at calculation of”.

-Fixes exception at calculation of stikstofgebruiksnorm for cultivations with sub_types
+Fix exception when calculating stikstofgebruiksnorm for cultivations with sub_types.
.changeset/crazy-onions-poke.md (1)

5-5: Fix typos and improve readability.

"calculationg" typo and phrasing makes the sentence hard to parse.

-At norms when a field has an error for calculationg norms show an error message on the card for that field, but do show a general error for the whole page and show still the fields that were able to calculate
+In norms, when a field has an error during calculation, show an error message on that field’s card; also show a general error for the whole page; still render the fields that were calculated successfully.
fdm-calculator/src/balance/nitrogen/supply/deposition.ts (3)

27-33: Avoid hardcoding year/region; derive when possible.

Minor: consider deriving year from timeFrame and guarding unsupported years. Keep the TODO but reduce future touchpoints.

-// Currently, only the year 2022 is available.
-// TODO: Add support for multiple years when data becomes available.
-const year = "2022"
-const region = "nl"
+// TODO: Add support for multiple years when data becomes available.
+const requestedYear = timeFrame.start.getUTCFullYear().toString()
+const year = ["2022"].includes(requestedYear) ? requestedYear : "2022"
+const region = "nl"

34-34: Comment step numbering is inconsistent.

You're using “Step 1”, “Step 3”, and “Step 4”. Renumber or simplify to avoid confusion.

-// Step 1: Create an array of promises to calculate deposition for each field concurrently.
+// Create an array of promises to calculate deposition for each field concurrently.

49-52: Skip GeoTIFF lookup when fraction is zero.

Small perf win: avoid I/O when timeframe has no overlap.

-        // Get the deposition value from the GeoTIFF using the new getTiffValue function.
-        const [longitude, latitude] = field.field.b_centroid
-        const value = await getGeoTiffValue(url, longitude, latitude)
+        // Skip lookup if timeframe has no overlap.
+        if (fraction.isZero()) {
+            return {
+                fieldId: field.field.b_id,
+                deposition: { total: new Decimal(0) },
+            }
+        }
+        // Get the deposition value from the GeoTIFF.
+        const [longitude, latitude] = field.field.b_centroid
+        const value = await getGeoTiffValue(url, longitude, latitude)
fdm-calculator/src/shared/public-data-url.ts (1)

1-3: Make base URL configurable and normalize trailing slash

Allow overriding via env and ensure a single trailing slash to avoid accidental // joins.

Apply:

-export function getFdmPublicDataUrl(): string {
-    return "https://storage.googleapis.com/fdm-public-data/"
-}
+export function getFdmPublicDataUrl(): string {
+    const fallback = "https://storage.googleapis.com/fdm-public-data/"
+    // process may be undefined in browsers – guard accordingly
+    const fromEnv =
+        typeof process !== "undefined" && process.env?.FDM_PUBLIC_DATA_URL
+            ? process.env.FDM_PUBLIC_DATA_URL
+            : undefined
+    const base = fromEnv ?? fallback
+    return base.endsWith("/") ? base : `${base}/`
+}
fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.test.ts (1)

116-134: Add an integration test: derogatie-vrije zone should suppress derogation

Add a case where farm.is_derogatie_bedrijf = true but the centroid is inside the derogatie-vrije zone; expect the non-derogation cap (170) and the appropriate source label.

fdm-app/app/components/blocks/norms/field-norms.tsx (1)

66-156: Guard against missing norms, add a11y to error, and standardize unit label

Avoid rendering “undefined” when norms are absent, mark the error container as an alert, and normalize “kg P2O5 / ha”.

Apply:

-                            {field.errorMessage ? (
-                                <div className="p-3 bg-red-50 rounded-lg border border-red-100 text-red-700">
+                            {field.errorMessage ? (
+                                <div
+                                    className="p-3 bg-red-50 rounded-lg border border-red-100 text-red-700"
+                                    role="alert"
+                                    aria-live="polite"
+                                >
...
-                            ) : (
+                            ) : field.norms ? (
...
-                                                <span className="text-sm font-normal text-gray-600">
-                                                    kg P2O5 /ha
-                                                </span>
+                                                <span className="text-sm font-normal text-gray-600">
+                                                    kg P2O5 / ha
+                                                </span>
...
-                            )}
+                            ) : (
+                                <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-100 text-gray-700">
+                                    Nog geen normen beschikbaar voor dit perceel.
+                                </div>
+                            )}
fdm-calculator/src/balance/nitrogen/index.ts (2)

309-335: Confirm rounding to integer output is intended.

convertDecimalToNumberRecursive rounds all values to integers. If fractional kg/ha are needed (UI or API), consider toDecimalPlaces with a defined precision or passing a rounding mode.

Example change:

-        return data.round().toNumber()
+        return data.toDecimalPlaces(1, Decimal.ROUND_HALF_UP).toNumber()

351-356: Avoid mutating input when sorting.

combineSoilAnalyses sorts soilAnalyses in place. If callers rely on original ordering elsewhere, make a shallow copy before sorting.

-    soilAnalyses.sort((a, b) => {
+    const analyses = [...soilAnalyses]
+    analyses.sort((a, b) => {
         return (
             new Date(b.b_sampling_date).getTime() -
             new Date(a.b_sampling_date).getTime()
         )
     })
...
-    for (const prop of propertiesToExtract) {
-        soilAnalysis[prop] =
-            soilAnalyses.find(
+    for (const prop of propertiesToExtract) {
+        soilAnalysis[prop] =
+            analyses.find(
                 (x) => x[prop] !== null && x[prop] !== undefined,
             )?.[prop] || null
     }
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm-data.ts (1)

920-920: Fix typos and label consistency.

Minor text issues that can confuse users or tests relying on exact labels:

  • Line 920: duplicated “Akkerbouwgewassen,”
  • Line 971: “Baldgewassen” → “Bladgewassen”
  • Line 742: missing space after comma
  • Line 2033: missing space after comma
  • Line 2439: trailing comma in label
-        cultivation_rvo_table2: "Akkerbouwgewassen, Akkerbouwgewassen, overig",
+        cultivation_rvo_table2: "Akkerbouwgewassen, overig",

-        cultivation_rvo_table2: "Baldgewassen, Spinazie volgteelt",
+        cultivation_rvo_table2: "Bladgewassen, Spinazie volgteelt",

-        cultivation_rvo_table2: "Akkerbouwgewassen, Graszaad,Westerwolds",
+        cultivation_rvo_table2: "Akkerbouwgewassen, Graszaad, Westerwolds",

-        cultivation_rvo_table2: "Bloembollengewassen,Lelie",
+        cultivation_rvo_table2: "Bloembollengewassen, Lelie",

-        cultivation_rvo_table2: "Boomkwekerijgewassen, Vruchtbomen, overig,",
+        cultivation_rvo_table2: "Boomkwekerijgewassen, Vruchtbomen, overig",

Also applies to: 971-971, 742-742, 2033-2033, 2439-2439

fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts (3)

24-39: Use 2025 layers (if available) and improve error context.

  • Paths point to 2024 datasets; confirm correct vintage for 2025 norms.
  • Error messages omit coordinates; include them for diagnostics.
-    const url = `${fdmPublicDataUrl}norms/nl/2024/gwbg.tiff`
+    const url = `${fdmPublicDataUrl}norms/nl/2025/gwbg.tiff`
...
-        default: {
-            throw new Error(`Unknown GWBG code: ${gwbgCode} for coordinates , `)
+        default: {
+            throw new Error(
+                `Unknown GWBG code: ${gwbgCode} for coordinates ${longitude}, ${latitude}`,
+            )

57-76: Same as GWBG: dataset year and error details.

Align to 2025 (if correct source) and include coordinates in error message.

-    const url = `${fdmPublicDataUrl}norms/nl/2024/natura2000.tiff`
+    const url = `${fdmPublicDataUrl}norms/nl/2025/natura2000.tiff`
...
-                `Unknown Natura2000 code: ${natura2000Code} for coordinates , `,
+                `Unknown Natura2000 code: ${natura2000Code} for coordinates ${longitude}, ${latitude}`,

89-115: Polish: error message typo and details.

Fix message grammar and add coordinates. Also confirm GeoTIFF value domain is strictly {0,1}; treat null as 0 if NoData outside NL is expected.

-                `Unknown  derogatieVrijeZoneCodes code: ${derogatieVrijeZoneCode} for coordinates , `,
+                `Unknown derogatie-vrije zone code: ${derogatieVrijeZoneCode} for coordinates ${longitude}, ${latitude}`,

Optional: handle null as false

-    switch (derogatieVrijeZoneCode) {
+    switch (derogatieVrijeZoneCode ?? 0) {
fdm-calculator/src/shared/geotiff.ts (1)

56-102: Add optional AbortSignal and document CRS assumption.

  • readRasters supports AbortController; accept an optional signal to allow upstream cancellation.
  • This math assumes the TIFF CRS matches the longitude/latitude passed (likely EPSG:4326). If sources vary, consider asserting/transforming.
-export async function getGeoTiffValue(
-    url: string,
-    longitude: number,
-    latitude: number,
-): Promise<number | null> {
+export async function getGeoTiffValue(
+    url: string,
+    longitude: number,
+    latitude: number,
+    options?: { signal?: AbortSignal },
+): Promise<number | null> {
...
-    const rasterData = await image.readRasters({ window })
+    const rasterData = await image.readRasters({ window, signal: options?.signal })

Optionally export a clearCache for tests/long-lived processes:

export function clearGeoTiffCaches() {
    tiffCache.clear()
    tiffPromiseCache.clear()
}
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (6)

30-47: Improve NV-gebied error handling and message clarity

  • Include coordinates in the error to aid debugging.
  • Consider handling null (outside TIFF/NoData) explicitly before the switch for clearer failures.

Apply this diff:

     const NVGebiedCode = await getGeoTiffValue(url, longitude, latitude)

+    if (NVGebiedCode === null) {
+        throw new Error(
+            `No NV-gebied value for coordinates ${longitude}, ${latitude} (outside coverage or NoData)`,
+        )
+    }
+
     switch (NVGebiedCode) {
         case 1: {
             return true
         }
         case 0: {
             return false
         }
         default: {
             throw new Error(
-                `Unknown NVGebied code: ${NVGebiedCode} for coordinates , `,
+                `Unknown NV-gebied code: ${NVGebiedCode} for coordinates ${longitude}, ${latitude}`,
             )
         }
     }

72-100: Handle null region values, fix typo, and verify dataset year

  • Add an explicit null check (outside TIFF/NoData).
  • Correct variable name typo and include coordinates in the error.
  • The URL references 2024 data in a 2025 module. Verify that this is intentional and that the 2024 grondsoorten.tiff is valid for 2025.

Apply this diff:

-    const grondoortCode = await getGeoTiffValue(url, longitude, latitude)
+    const grondsoortCode = await getGeoTiffValue(url, longitude, latitude)
+
+    if (grondsoortCode === null) {
+        throw new Error(
+            `No grondsoorten value for coordinates ${longitude}, ${latitude} (outside coverage or NoData)`,
+        )
+    }

-    switch (grondoortCode) {
+    switch (grondsoortCode) {
         case 1: {
             return "klei"
         }
         case 2: {
             return "loess"
         }
         case 3: {
             return "veen"
         }
         case 4: {
             return "zand_nwc"
         }
         case 5: {
             return "zand_zuid"
         }
         default: {
             throw new Error(
-                `Unknown region code: ${grondoortCode} for coordinates , `,
+                `Unknown region code: ${grondsoortCode} for coordinates ${longitude}, ${latitude}`,
             )
         }
     }

Also confirm that:

  • norms/nl/2024/grondsoorten.tiff is the correct authoritative source for 2025, or update to a 2025 path/file if available.

251-294: Unify previous-year checks with a single YEAR constant

Multiple branches hardcode 2024. Consider using a single YEAR (2025) and derive PREV_YEAR to avoid drift and ease future maintenance.

Example:

const YEAR = 2025
const PREV_YEAR = YEAR - 1
// then replace <= 2024 with <= PREV_YEAR

Based on learnings.


312-318: Volgteelt logic is still TODO

Issue #276 mentions selecting "1e teelt" vs "volgteelt". You only implemented "1e teelt". Track or implement "volgteelt" selection (e.g., any additional same-year bladgewassen after hoofdteelt).

I can propose a minimal, testable volgteelt heuristic based on sow dates and hoofdteelt order if desired.


576-582: Selecting the most specific standard is fragile

Current heuristic picks the first standard that happens to have any sub_types with omschrijving or varieties. This may not correlate with the actual sub-type/variety for the cultivation.

Refactor to evaluate each candidate:

  • For each ns in matchingStandards, compute subTypeOmschrijving via determineSubTypeOmschrijving(cultivation, ns, is_derogatie_bedrijf, cultivations).
  • Pass that to getNormsForCultivation(ns, b_lu_end, subTypeOmschrijving).
  • Choose the first with a defined NormsByRegion; otherwise fall back.

This avoids picking the wrong standard when multiple entries exist.


630-634: Keep source and korting separate (optional API polish)

Appending kortingDescription to normSource mixes concerns. Consider returning kortingDescription as a separate field for clearer UI rendering and i18n.

If changing API isn’t feasible now, keep as is.

fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx (1)

36-45: Avoid duplicating FieldNorm type

This duplicates the FieldNorm shape from components/blocks/norms/field-norms.tsx. Prefer importing the type to keep props in sync and reduce drift.

For example:

  • export type FieldNorm from the component module and import it here.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4511ee8 and e2d2263.

📒 Files selected for processing (22)
  • .changeset/crazy-onions-poke.md (1 hunks)
  • .changeset/every-shoes-boil.md (1 hunks)
  • .changeset/kind-singers-arrive.md (1 hunks)
  • .changeset/light-rockets-fly.md (1 hunks)
  • .changeset/seven-crabs-cross.md (1 hunks)
  • .changeset/tame-cars-kneel.md (1 hunks)
  • fdm-app/app/components/blocks/norms/farm-norms.tsx (1 hunks)
  • fdm-app/app/components/blocks/norms/field-norms.tsx (2 hunks)
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx (8 hunks)
  • fdm-calculator/src/balance/nitrogen/index.test.ts (0 hunks)
  • fdm-calculator/src/balance/nitrogen/index.ts (1 hunks)
  • fdm-calculator/src/balance/nitrogen/supply/deposition.test.ts (1 hunks)
  • fdm-calculator/src/balance/nitrogen/supply/deposition.ts (3 hunks)
  • fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.test.ts (2 hunks)
  • fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts (3 hunks)
  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm-data.ts (3 hunks)
  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.test.ts (2 hunks)
  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (7 hunks)
  • fdm-calculator/src/norms/nl/2025/types.d.ts (1 hunks)
  • fdm-calculator/src/shared/geotiff.ts (1 hunks)
  • fdm-calculator/src/shared/public-data-url.ts (1 hunks)
  • fdm-calculator/tsconfig.json (1 hunks)
💤 Files with no reviewable changes (1)
  • fdm-calculator/src/balance/nitrogen/index.test.ts
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2025-09-23T10:02:32.123Z
Learnt from: BoraIneviNMI
PR: SvenVw/fdm#272
File: fdm-app/app/routes/farm.create.$b_id_farm.$calendar.fertilizers.$b_lu_catalogue.manage.$p_id.tsx:151-164
Timestamp: 2025-09-23T10:02:32.123Z
Learning: The getFertilizer function from svenvw/fdm-core throws an exception if the fertilizer doesn't exist, rather than returning null or undefined.

Applied to files:

  • .changeset/light-rockets-fly.md
  • .changeset/every-shoes-boil.md
📚 Learning: 2025-08-13T10:33:05.313Z
Learnt from: SvenVw
PR: SvenVw/fdm#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:

  • .changeset/tame-cars-kneel.md
  • .changeset/seven-crabs-cross.md
📚 Learning: 2025-07-21T12:06:07.070Z
Learnt from: SvenVw
PR: SvenVw/fdm#156
File: fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts:295-303
Timestamp: 2025-07-21T12:06:07.070Z
Learning: Functions in the fdm-calculator with "NL2025" in their names are specifically designed for Netherlands 2025 agricultural norms calculation and hardcoded 2025 dates are appropriate in this context, as different years would have separate calculation modules.

Applied to files:

  • .changeset/tame-cars-kneel.md
  • .changeset/seven-crabs-cross.md
  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.test.ts
  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
📚 Learning: 2024-12-11T12:09:35.540Z
Learnt from: SvenVw
PR: SvenVw/fdm#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-calculator/tsconfig.json
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
PR: SvenVw/fdm#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/norms/farm-norms.tsx
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
PR: SvenVw/fdm#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/norms/farm-norms.tsx
📚 Learning: 2025-09-23T12:37:58.700Z
Learnt from: SvenVw
PR: SvenVw/fdm#274
File: fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx:113-148
Timestamp: 2025-09-23T12:37:58.700Z
Learning: In the FDM application, the current field data fetching implementation using Promise.all with individual API calls (getCultivations, getFertilizerApplications, getCurrentSoilData) performs acceptably even with farms containing 90+ fields. No performance issues have been observed in practice with this approach.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx
📚 Learning: 2025-08-14T14:31:55.384Z
Learnt from: SvenVw
PR: SvenVw/fdm#236
File: fdm-calculator/src/balance/nitrogen/index.ts:173-0
Timestamp: 2025-08-14T14:31:55.384Z
Learning: In nitrogen balance calculations for agricultural systems, the balance should only include ammonia emissions (emission.ammonia.total) and should not include nitrate leaching from the emission calculation. The nitrate component (emission.nitrate) should be excluded from the balance formula.

Applied to files:

  • fdm-calculator/src/balance/nitrogen/index.ts
🧬 Code graph analysis (6)
fdm-calculator/src/balance/nitrogen/supply/deposition.ts (1)
fdm-calculator/src/shared/geotiff.ts (1)
  • getGeoTiffValue (56-102)
fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.test.ts (1)
fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts (1)
  • isFieldInDerogatieVrijeZone (89-115)
fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts (3)
fdm-calculator/src/shared/public-data-url.ts (1)
  • getFdmPublicDataUrl (1-3)
fdm-calculator/src/shared/geotiff.ts (1)
  • getGeoTiffValue (56-102)
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (1)
  • isFieldInNVGebied (27-49)
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.test.ts (2)
fdm-calculator/src/norms/nl/2025/types.d.ts (2)
  • NL2025NormsInput (14-25)
  • NL2025NormsInputForCultivation (6-9)
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (1)
  • getNL2025StikstofGebruiksNorm (525-635)
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (5)
fdm-calculator/src/shared/public-data-url.ts (1)
  • getFdmPublicDataUrl (1-3)
fdm-calculator/src/shared/geotiff.ts (1)
  • getGeoTiffValue (56-102)
fdm-calculator/src/norms/nl/2025/types.d.ts (4)
  • RegionKey (146-146)
  • NormsByRegion (151-153)
  • NL2025NormsInputForCultivation (6-9)
  • NitrogenStandard (80-141)
fdm-calculator/src/norms/nl/2025/hoofdteelt.ts (1)
  • determineNL2025Hoofdteelt (19-73)
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm-data.ts (1)
  • nitrogenStandardsData (1-2625)
fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx (5)
fdm-app/app/components/blocks/norms/field-norms.tsx (1)
  • FieldNorm (15-24)
fdm-calculator/src/norms/nl/2025/types.d.ts (1)
  • GebruiksnormResult (159-169)
fdm-calculator/src/norms/farm.ts (1)
  • AggregatedNormsToFarmLevel (31-44)
fdm-calculator/src/norms/index.ts (1)
  • createFunctionsForNorms (7-21)
fdm-app/app/components/blocks/norms/farm-norms.tsx (1)
  • FarmNorms (20-93)
🪛 Biome (2.1.2)
fdm-app/app/components/blocks/norms/farm-norms.tsx

[error] 41-41: Avoid using the index of an array as key property in an element.

This is the source of the key value.

The order of the items may change, and this also affects performances and component state.
Check the React documentation.

(lint/suspicious/noArrayIndexKey)

fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts

[error] 153-153: This variable implicitly has the any type.

Variable declarations without type annotation and initialization implicitly have the any type. Declare a type or initialize the variable with some value.

(lint/suspicious/noImplicitAnyLet)

🔇 Additional comments (22)
fdm-calculator/src/balance/nitrogen/supply/deposition.ts (1)

50-51: Confirm GeoTIFF CRS matches lon/lat. Ensure ntot_2022.tiff is in EPSG:4326 (use gdalinfo or image.getGeoKeys()); if it’s in a projected CRS (e.g. EPSG:28992), reproject b_centroid to that CRS or serve a WGS84‐aligned TIFF.

fdm-calculator/tsconfig.json (1)

22-22: No local non-relative imports in fdm-calculator — baseUrl is safe here

Scanned fdm-calculator/src: imports are external/scoped packages (e.g. @svenvw/fdm-core, decimal.js); no bare non-relative local imports that would require TS baseUrl at runtime were found. If you later introduce non-relative internal imports, ensure your bundler or a runtime resolver (tsconfig-paths / plugin) rewrites them in the build output.

.changeset/seven-crabs-cross.md (1)

2-5: LGTM on changeset entry

Minor bump and concise description look good.

.changeset/light-rockets-fly.md (1)

2-5: LGTM on changeset entry

Patch bump and message are clear.

fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.test.ts (2)

80-82: Verify label consistency: “Akkerbouwgewas” vs “Akkerbouwgewassen”

Ensure the source string for pootaardappelen matches the dataset’s cultivation_rvo_table2 naming convention used elsewhere (most tests expect “Akkerbouwgewassen, …”).


291-568: Add tests for rapeseed winter/summer and bulb variety sub-types; postpone spinach/lettuce volgteelt

  • Rapeseed (nl_1922 vs nl_1923): verify “winter” vs “zomer” sub-type in normSource and normValue.
  • Flower bulbs: verify “grofbollig” vs “fijnbollig” variety-based omschrijving.
  • Spinach/Lettuce volgteelt: logic still TODO—implement before testing.
fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.test.ts (1)

2-5: Public API import looks good

Exposing and testing isFieldInDerogatieVrijeZone is consistent with the calculator module changes.

fdm-app/app/components/blocks/norms/field-norms.tsx (1)

15-24: Exported FieldNorm shape looks good

Optional norms and errorMessage enable the new UI states without breaking existing usage.

fdm-calculator/src/balance/nitrogen/supply/deposition.test.ts (1)

4-4: All imports of getFdmPublicDataUrl updated
No remaining imports reference a non-shared path; every usage now imports from shared/public-data-url.

fdm-calculator/src/balance/nitrogen/index.ts (2)

24-24: Good move: centralized public data URL.

Importing getFdmPublicDataUrl avoids hardcoded URLs and keeps sources consistent.


208-214: Balance uses ammonia only (correct).

Summing emission.ammonia.total (excluding nitrate) matches the intended balance definition.

Based on learnings

Also applies to: 268-269

fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm-data.ts (3)

186-223: Potato (consumption) refactor to sub_types looks solid.

Consolidating high/low/overig under sub_types with varieties lists improves lookup clarity. Please ensure variety names are deduplicated and consistently cased to avoid ambiguous matches.


246-283: Potato (seed) sub_types structure LGTM.

Same note on de-duplication and consistent casing in varieties.


473-494: Mais derogatie/non-derogatie split is clear. Verify caller sub-type selection.

New sub_types require the caller to pass a matching omschrijving. Confirm getNormsForCultivation and getNL2025StikstofGebruiksNorm set subTypeOmschrijving accordingly (per PR objective).

Would you like a small helper that picks sub_types by omschrijving with a tolerant comparator (case/diacritics)?

fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts (2)

153-163: Parallel checks via Promise.all are appropriate.

Good use of concurrency and shared TIFF caching to keep latency down.


168-178: Logic addition for derogatie-vrije zone is in the right precedence.

Applying 170 cap before NV aligns with the rule intent.

fdm-calculator/src/shared/geotiff.ts (1)

21-46: Solid, deduplicated TIFF fetch with in-memory cache.

Prevents duplicate downloads and reduces CPU; error cleanup is correct.

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

202-216: Farm aggregation guard looks good

Filtering to validFieldNorms before aggregation prevents runtime errors when some fields fail. Nice.


285-292: Async data destructuring is correct

use(loaderData.asyncData) shape matches the loader return, including new error aggregation fields.


358-362: FarmNorms receives new error-state props

Prop wiring for hasFieldNormErrors and fieldErrorMessages aligns with the new UI behavior.

fdm-calculator/src/norms/nl/2025/types.d.ts (2)

104-111: Top-level norms still present; confirm data alignment

The NitrogenStandard interface still exposes optional top-level norms. Ensure the data file matches this shape for standards without sub_types so getNormsForCultivation’s default path remains valid.


118-140: Varieties on sub_types is a good fit for potato handling

Adding varieties?: string[] under sub_types matches the new selection logic using determineSubTypeOmschrijving.

Comment thread fdm-app/app/components/blocks/norms/farm-norms.tsx
Comment thread fdm-app/app/components/blocks/norms/farm-norms.tsx Outdated
Comment thread fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx
Comment thread fdm-calculator/src/balance/nitrogen/supply/deposition.ts Outdated
Comment thread fdm-calculator/src/balance/nitrogen/supply/deposition.ts Outdated
Comment thread fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
Comment thread fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
@coderabbitai coderabbitai Bot added branch:development Issue only affecting development, not the main branch (yet) fdm-app labels Sep 29, 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: 3

🧹 Nitpick comments (4)
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (3)

233-294: Factor out a single norm-year constant.

All the “previous year” lookups now compare against a hard-coded 2024. That literal shows up in multiple branches (lucerne, grass for industrial processing, graszaad, roodzwenkgras) and will be easy to miss next time we roll the module forward. Please extract a shared constant (e.g. const NORM_YEAR = 2025) at the top of the file and derive the previousYear from it so future updates stay consistent.


44-46: Include coordinates in the error message.

If the GeoTIFF returns an unexpected code (including null), the thrown error currently renders as Unknown NVGebied code: … for coordinates ,. Please interpolate the longitude & latitude so we can investigate bad tiles or out-of-bounds centroids more easily.


96-97: Add coordinates to the region error as well.

Same issue here—the fallback error omits the centroid values, which makes debugging harder. Please include both numbers in the message.

fdm-calculator/src/balance/nitrogen/supply/deposition.ts (1)

45-61: Short-circuit GeoTIFF fetch when fraction is zero.

When the effective timeframe doesn’t overlap the balance period, fraction is zero but we still read the raster. We can return immediately and avoid an unnecessary GeoTIFF lookup for those fields.

         const days = differenceInCalendarDays(effectiveEnd, effectiveStart)
         const fraction =
             days >= 0 ? new Decimal(days).add(1).dividedBy(365) : new Decimal(0)

-        // Get the deposition value from the GeoTIFF using the new getTiffValue function.
+        if (fraction.isZero()) {
+            return {
+                fieldId: field.field.b_id,
+                deposition: { total: new Decimal(0) },
+            }
+        }
+
+        // Get the deposition value from the GeoTIFF using the new getGeoTiffValue function.
         const [longitude, latitude] = field.field.b_centroid
         const value = await getGeoTiffValue(url, longitude, latitude)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2d2263 and 992c788.

📒 Files selected for processing (4)
  • fdm-app/app/components/blocks/norms/farm-norms.tsx (1 hunks)
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx (9 hunks)
  • fdm-calculator/src/balance/nitrogen/supply/deposition.ts (3 hunks)
  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (7 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: SvenVw
PR: SvenVw/fdm#156
File: fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts:295-303
Timestamp: 2025-07-21T12:06:07.070Z
Learning: Functions in the fdm-calculator with "NL2025" in their names are specifically designed for Netherlands 2025 agricultural norms calculation and hardcoded 2025 dates are appropriate in this context, as different years would have separate calculation modules.
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
PR: SvenVw/fdm#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/norms/farm-norms.tsx
📚 Learning: 2025-08-11T12:24:32.200Z
Learnt from: SvenVw
PR: SvenVw/fdm#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/components/blocks/norms/farm-norms.tsx
  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
PR: SvenVw/fdm#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/norms/farm-norms.tsx
📚 Learning: 2025-09-23T12:37:58.700Z
Learnt from: SvenVw
PR: SvenVw/fdm#274
File: fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx:113-148
Timestamp: 2025-09-23T12:37:58.700Z
Learning: In the FDM application, the current field data fetching implementation using Promise.all with individual API calls (getCultivations, getFertilizerApplications, getCurrentSoilData) performs acceptably even with farms containing 90+ fields. No performance issues have been observed in practice with this approach.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx
📚 Learning: 2025-07-21T12:06:07.070Z
Learnt from: SvenVw
PR: SvenVw/fdm#156
File: fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts:295-303
Timestamp: 2025-07-21T12:06:07.070Z
Learning: Functions in the fdm-calculator with "NL2025" in their names are specifically designed for Netherlands 2025 agricultural norms calculation and hardcoded 2025 dates are appropriate in this context, as different years would have separate calculation modules.

Applied to files:

  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
📚 Learning: 2025-08-11T11:55:26.053Z
Learnt from: SvenVw
PR: SvenVw/fdm#233
File: fdm-app/app/integrations/nmi.ts:54-0
Timestamp: 2025-08-11T11:55:26.053Z
Learning: The NMI API Estimates endpoint (`https://api.nmi-agro.nl/estimates`) always returns the fields `b_gwl_ghg`, `b_gwl_glg`, and `cultivations` according to its specification. These fields should be kept as required (not optional) in the TypeScript return type and Zod validation schema in `fdm-app/app/integrations/nmi.ts`.

Applied to files:

  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
🧬 Code graph analysis (3)
fdm-calculator/src/balance/nitrogen/supply/deposition.ts (1)
fdm-calculator/src/shared/geotiff.ts (1)
  • getGeoTiffValue (56-102)
fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx (6)
fdm-app/app/components/blocks/norms/field-norms.tsx (1)
  • FieldNorm (15-24)
fdm-calculator/src/norms/nl/2025/types.d.ts (1)
  • GebruiksnormResult (159-169)
fdm-calculator/src/index.ts (3)
  • GebruiksnormResult (36-36)
  • AggregatedNormsToFarmLevel (26-26)
  • createFunctionsForNorms (24-24)
fdm-calculator/src/norms/farm.ts (1)
  • AggregatedNormsToFarmLevel (31-44)
fdm-calculator/src/norms/index.ts (1)
  • createFunctionsForNorms (7-21)
fdm-app/app/components/blocks/norms/farm-norms.tsx (1)
  • FarmNorms (20-102)
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (4)
fdm-calculator/src/shared/public-data-url.ts (1)
  • getFdmPublicDataUrl (1-3)
fdm-calculator/src/shared/geotiff.ts (1)
  • getGeoTiffValue (56-102)
fdm-calculator/src/norms/nl/2025/types.d.ts (3)
  • RegionKey (146-146)
  • NormsByRegion (151-153)
  • NL2025NormsInputForCultivation (6-9)
fdm-calculator/src/norms/nl/2025/hoofdteelt.ts (1)
  • determineNL2025Hoofdteelt (19-73)

Comment thread fdm-app/app/components/blocks/norms/farm-norms.tsx Outdated
Comment thread fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx Outdated
Comment thread fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
@SvenVw SvenVw requested a review from gerardhros September 29, 2025 08:27
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.norms.tsx (1)

36-45: Reuse the shared FieldNorm type instead of duplicating it.

We already export this interface from field-norms.tsx; redefining it here increases the chance the shapes drift apart (for example if a new property is added later). Please import the shared type and drop this duplicate declaration.

fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts (3)

24-26: GWBG: verify dataset year (2024 vs 2025) and handle null explicitly

  • Year: This NL2025 module references norms/nl/2024/gwbg.tiff. If that’s intentional (no 2025 update), add a short comment. If a 2025 raster exists, switch to it. Based on learnings.
  • Nulls: getGeoTiffValue can return null (OOB/NoData). Handling it explicitly yields clearer errors.
-    const url = `${fdmPublicDataUrl}norms/nl/2024/gwbg.tiff`
+    const url = `${fdmPublicDataUrl}norms/nl/2024/gwbg.tiff` // NOTE: keep if 2024 is authoritative for 2025; otherwise switch to 2025.

-    switch (gwbgCode) {
+    switch (gwbgCode) {
       case 1: {
         return true
       }
       case 0: {
         return false
       }
+      case null: {
+        throw new Error(
+          `GWBG NoData/outside raster extent for coordinates ${longitude}, ${latitude}`,
+        )
+      }
       default: {
         throw new Error(
           `Unknown GWBG code: ${gwbgCode} for coordinates ${longitude}, ${latitude}`,
         )
       }
     }

Also applies to: 30-41


59-61: Natura2000: same as GWBG — confirm 2024 raster and add null handling

Mirror the above: document 2024 usage (or update to 2025) and treat null explicitly.

-    const url = `${fdmPublicDataUrl}norms/nl/2024/natura2000.tiff`
+    const url = `${fdmPublicDataUrl}norms/nl/2024/natura2000.tiff` // NOTE: confirm year.

-    switch (natura2000Code) {
+    switch (natura2000Code) {
       case 1: {
         return true
       }
       case 0: {
         return false
       }
+      case null: {
+        throw new Error(
+          `Natura2000 NoData/outside raster extent for coordinates ${longitude}, ${latitude}`,
+        )
+      }
       default: {
         throw new Error(
           `Unknown Natura2000 code: ${natura2000Code} for coordinates ${longitude}, ${latitude}`,
         )
       }
     }

Also applies to: 65-78


155-165: Good parallelization; decide on fail-fast vs per-layer error collection

Promise.all fails on the first rejection. If the UI should show per-layer errors while still proceeding with available signals, consider Promise.allSettled with explicit aggregation. If fail-fast is desired for data integrity, keep as-is.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 992c788 and b320747.

📒 Files selected for processing (8)
  • .changeset/crazy-onions-poke.md (1 hunks)
  • .changeset/every-shoes-boil.md (1 hunks)
  • .changeset/kind-singers-arrive.md (1 hunks)
  • .changeset/tame-cars-kneel.md (1 hunks)
  • fdm-app/app/components/blocks/norms/farm-norms.tsx (1 hunks)
  • fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx (9 hunks)
  • fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts (3 hunks)
  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (7 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .changeset/kind-singers-arrive.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • .changeset/crazy-onions-poke.md
  • .changeset/every-shoes-boil.md
🧰 Additional context used
🧠 Learnings (9)
📚 Learning: 2025-08-13T10:33:05.313Z
Learnt from: SvenVw
PR: SvenVw/fdm#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:

  • .changeset/tame-cars-kneel.md
📚 Learning: 2025-07-21T12:06:07.070Z
Learnt from: SvenVw
PR: SvenVw/fdm#156
File: fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts:295-303
Timestamp: 2025-07-21T12:06:07.070Z
Learning: Functions in the fdm-calculator with "NL2025" in their names are specifically designed for Netherlands 2025 agricultural norms calculation and hardcoded 2025 dates are appropriate in this context, as different years would have separate calculation modules.

Applied to files:

  • .changeset/tame-cars-kneel.md
  • fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts
  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
📚 Learning: 2025-09-23T12:37:58.700Z
Learnt from: SvenVw
PR: SvenVw/fdm#274
File: fdm-app/app/routes/farm.$b_id_farm.$calendar.field._index.tsx:113-148
Timestamp: 2025-09-23T12:37:58.700Z
Learning: In the FDM application, the current field data fetching implementation using Promise.all with individual API calls (getCultivations, getFertilizerApplications, getCurrentSoilData) performs acceptably even with farms containing 90+ fields. No performance issues have been observed in practice with this approach.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx
📚 Learning: 2025-01-31T15:05:14.310Z
Learnt from: SvenVw
PR: SvenVw/fdm#67
File: fdm-app/app/routes/farm.create.$b_id_farm.fields.$b_id.tsx:601-610
Timestamp: 2025-01-31T15:05:14.310Z
Learning: The `updateField` function in fdm-core has optional parameters after `fdm` and `b_id`. The TypeScript definitions might show 8 required parameters due to a potential version mismatch.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx
📚 Learning: 2025-01-31T15:34:20.850Z
Learnt from: SvenVw
PR: SvenVw/fdm#67
File: fdm-app/app/routes/farm.create.$b_id_farm.fields.$b_id.tsx:601-610
Timestamp: 2025-01-31T15:34:20.850Z
Learning: The `updateField` function in fdm-core has optional parameters that don't need to be passed as undefined. Only `fdm` and `b_id` are required.

Applied to files:

  • fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx
📚 Learning: 2025-08-11T11:55:26.053Z
Learnt from: SvenVw
PR: SvenVw/fdm#233
File: fdm-app/app/integrations/nmi.ts:54-0
Timestamp: 2025-08-11T11:55:26.053Z
Learning: The NMI API Estimates endpoint (`https://api.nmi-agro.nl/estimates`) always returns the fields `b_gwl_ghg`, `b_gwl_glg`, and `cultivations` according to its specification. These fields should be kept as required (not optional) in the TypeScript return type and Zod validation schema in `fdm-app/app/integrations/nmi.ts`.

Applied to files:

  • fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
📚 Learning: 2025-08-11T12:24:32.200Z
Learnt from: SvenVw
PR: SvenVw/fdm#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-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
  • fdm-app/app/components/blocks/norms/farm-norms.tsx
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
PR: SvenVw/fdm#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/norms/farm-norms.tsx
📚 Learning: 2025-01-09T16:03:37.764Z
Learnt from: SvenVw
PR: SvenVw/fdm#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/norms/farm-norms.tsx
🧬 Code graph analysis (3)
fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts (3)
fdm-calculator/src/shared/public-data-url.ts (1)
  • getFdmPublicDataUrl (1-3)
fdm-calculator/src/shared/geotiff.ts (1)
  • getGeoTiffValue (56-102)
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (1)
  • isFieldInNVGebied (27-49)
fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx (5)
fdm-app/app/components/blocks/norms/field-norms.tsx (1)
  • FieldNorm (15-24)
fdm-calculator/src/norms/nl/2025/types.d.ts (1)
  • GebruiksnormResult (159-169)
fdm-calculator/src/norms/farm.ts (1)
  • AggregatedNormsToFarmLevel (31-44)
fdm-calculator/src/norms/index.ts (1)
  • createFunctionsForNorms (7-21)
fdm-app/app/components/blocks/norms/farm-norms.tsx (1)
  • FarmNorms (20-102)
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts (5)
fdm-calculator/src/shared/public-data-url.ts (1)
  • getFdmPublicDataUrl (1-3)
fdm-calculator/src/shared/geotiff.ts (1)
  • getGeoTiffValue (56-102)
fdm-calculator/src/norms/nl/2025/types.d.ts (4)
  • RegionKey (146-146)
  • NormsByRegion (151-153)
  • NitrogenStandard (80-141)
  • NL2025NormsInputForCultivation (6-9)
fdm-calculator/src/norms/nl/2025/hoofdteelt.ts (1)
  • determineNL2025Hoofdteelt (19-73)
fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm-data.ts (1)
  • nitrogenStandardsData (1-2625)
🔇 Additional comments (3)
fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts (3)

7-8: Centralized helpers — LGTM

Moving to shared GeoTIFF + public-data URL utilities is the right direction.


12-20: Clarify CRS assumption in docs (WGS84 vs RD/EPSG:28992)

These lookups assume the centroid coordinates are in the same CRS as the GeoTIFFs. Please confirm TIFFs are EPSG:4326 (lon/lat). If they’re RD New (EPSG:28992) you’ll need a transform before sampling.


177-179: Precedence includes Derogatie‑vrije zone before NV — looks right

This ordering matches typical rule priority. Please confirm with the policy source for 2025 to avoid a precedence regression.

Comment thread fdm-calculator/src/norms/nl/2025/dierlijke-mest-gebruiksnorm.ts
Comment thread fdm-calculator/src/norms/nl/2025/stikstofgebruiksnorm.ts
Copy link
Copy Markdown
Collaborator

@gerardhros gerardhros left a comment

Choose a reason for hiding this comment

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

LGTM.

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) enhancement New feature or request fdm-app fdm-calculator

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sub-type handling is incorrectly implemented for various cultivations in getNormsForCultivation

2 participants