Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/grumpy-mirrors-see.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nmi-agro/fdm-app": minor
---

Added calculation for Nitrogen Supplying Capacity (NLV / `d_n_supply_base`) across soil cards, the field overview dashboard, fields table, and the soil analysis atlas map layer.
5 changes: 5 additions & 0 deletions .changeset/rich-masks-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nmi-agro/fdm-core": minor
---

Add `d_n_supply_base` as soil parameter (NLV, mineralisation by soil organic matter from soil) that can have a 'calculated' source
5 changes: 5 additions & 0 deletions .changeset/swift-ears-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nmi-agro/fdm-calculator": minor
---

Add function `calculateNlv` to calculate the nitrogen mineralization from soil organic matter. Rename `calculateNlvSupplyBySom` to `calculateNlvSupplyIncreaseBySomPotential`
1 change: 1 addition & 0 deletions fdm-app/app/components/blocks/atlas/atlas-soil-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const GRADIENT_SHADED_SOIL_PARAMETERS = {
a_com_fr: "carbon_ratio",
a_cu_cc: "copper",
a_density_sa: "earth_heavy",
d_n_supply_base: "nitrogen",
a_fe_ox: "iron",
a_k_cc: "potassium",
a_k_co: "potassium",
Expand Down
14 changes: 9 additions & 5 deletions fdm-app/app/components/blocks/field-dashboard/tiles.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { format } from "date-fns"
import { nl } from "date-fns/locale"
import {
Calculator,
CheckCircle2,
ClipboardCheck,
Leaf,
Expand Down Expand Up @@ -57,6 +58,7 @@ function formatNumberLabel(value: number | null | undefined, unit?: string) {
}

function soilSourceIcon(source: string | null) {
if (source === "calculated") return Calculator
if (source === "nl-other-nmi") return Sparkles
if (source && source !== "other") return Microscope
return User
Expand Down Expand Up @@ -743,11 +745,13 @@ export function FieldDashboardSoilParametersTile({ dashboard, tile }: FieldDashb
<div className="text-muted-foreground mt-3 flex items-center gap-2 text-xs">
<Icon className="size-3.5" />
<span className="truncate">
{card.source === "nl-other-nmi"
? "Geschat met NMI BodemSchat"
: card.source && card.source !== "other"
? `Gemeten door ${card.sourceLabel}`
: "Onbekende bron"}
{card.source === "calculated"
? "Berekend"
: card.source === "nl-other-nmi"
? "Geschat met NMI BodemSchat"
: card.source && card.source !== "other"
? `Gemeten door ${card.sourceLabel}`
: "Onbekende bron"}
</span>
</div>
</div>
Expand Down
20 changes: 20 additions & 0 deletions fdm-app/app/components/blocks/fields/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type FieldExtended = {
cultivations: Cultivation[]
cultivationSuggestion?: CultivationSuggestion
fertilizers: Fertilizer[]
d_n_supply_base: number | null
a_som_loi: number | null
b_soiltype_agr: string | null
b_area: number
Expand Down Expand Up @@ -178,6 +179,25 @@ export function buildColumns(b_id_farm: string, calendar: string): ColumnDef<Fie
cell: (props) => <BufferStripCheckbox {...props} />,
enableHiding: true, // Enable hiding for mobile
},
{
accessorKey: "d_n_supply_base",
enableSorting: true,
sortingFn: "alphanumeric",
header: ({ column }) => {
return <DataTableColumnHeader column={column} title="NLV" />
},
enableHiding: true, // Enable hiding for mobile
cell: ({ row }) => {
const field = row.original
return (
<p className="text-muted-foreground">
{field.d_n_supply_base !== null
? `${Math.round(field.d_n_supply_base)} kg N/ha`
: "-"}
</p>
)
},
},
{
accessorKey: "a_som_loi",
enableSorting: true,
Expand Down
30 changes: 19 additions & 11 deletions fdm-app/app/components/blocks/soil/cards.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CurrentSoilData, SoilParameterDescription } from "@nmi-agro/fdm-core"
import { format } from "date-fns/format"
import { nl } from "date-fns/locale/nl"
import { Calendar, ExternalLink, Microscope, Pencil, Sparkles, User } from "lucide-react"
import { Calculator, Calendar, ExternalLink, Microscope, Pencil, Sparkles, User } from "lucide-react"
import { NavLink } from "react-router"
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"
import { Separator } from "~/components/ui/separator"
Expand All @@ -11,7 +11,7 @@ import { cn } from "~/lib/utils"
const SOIL_GROUPS = [
{
title: "Basis",
parameters: ["b_soiltype_agr", "a_som_loi", "a_ph_cc", "b_gwl_class"],
parameters: ["b_soiltype_agr", "a_som_loi", "d_n_supply_base", "a_ph_cc", "b_gwl_class"],
},
{
title: "Nutriënten",
Expand Down Expand Up @@ -88,7 +88,8 @@ function SoilDataCard({
sourceLabel: string
canModify: boolean
}) {
const EditIcon = canModify ? Pencil : ExternalLink
const isEditable = canModify && source !== "calculated"
const EditIcon = isEditable ? Pencil : ExternalLink
return (
<Card className="hover:bg-accent/5 flex h-full flex-col transition-colors">
<CardHeader className="flex flex-row items-start justify-between space-y-0 space-x-2 pb-2">
Expand All @@ -112,7 +113,7 @@ function SoilDataCard({
<EditIcon className="text-muted-foreground h-full w-full text-xs opacity-50 transition-opacity hover:opacity-100" />
</NavLink>
</TooltipTrigger>
<TooltipContent>{canModify ? "Bewerken" : "Bekijken"}</TooltipContent>
<TooltipContent>{isEditable ? "Bewerken" : "Bekijken"}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : null}
Expand Down Expand Up @@ -145,7 +146,9 @@ function SoilDataCard({
!source ? "invisible" : "",
)}
>
{source === "nl-other-nmi" ? (
{source === "calculated" ? (
<Calculator className="h-3 w-3 shrink-0" />
) : source === "nl-other-nmi" ? (
<Sparkles className="h-3 w-3 shrink-0" />
) : source === "other" || !source ? (
<User className="h-3 w-3 shrink-0" />
Expand All @@ -156,11 +159,13 @@ function SoilDataCard({
</div>
</TooltipTrigger>
<TooltipContent side="bottom">
{source === "nl-other-nmi"
? `Geschat met ${sourceLabel}`
: source === "other" || !source
? "Onbekende bron"
: `Gemeten door ${sourceLabel}`}
{source === "calculated"
? "Berekend"
: source === "nl-other-nmi"
? `Geschat met ${sourceLabel}`
: source === "other" || !source
? "Onbekende bron"
: `Gemeten door ${sourceLabel}`}
</TooltipContent>
</Tooltip>
</TooltipProvider>
Expand Down Expand Up @@ -213,7 +218,10 @@ export function SoilDataCards({
{groupCards.map((card) => {
const sourceParam = soilParameterDescription.find((x) => x.parameter === "a_source")
const sourceOption = sourceParam?.options?.find((x) => x.value === card.source)
const sourceLabel = sourceOption?.label || card.source || "Onbekend"
const sourceLabel =
card.source === "calculated"
? "Berekend"
: sourceOption?.label || card.source || "Onbekend"

return (
<SoilDataCard
Expand Down
54 changes: 54 additions & 0 deletions fdm-app/app/lib/soil.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { calculateNlv } from "@nmi-agro/fdm-calculator"
import type { CurrentSoilData } from "@nmi-agro/fdm-core"

/**
* Enriches current soil data with the calculated base Nitrogen Supplying Capacity (d_n_supply_base / NLV)
* if it is not already present and if soil organic matter (a_som_loi) is available.
*
* Sets the source to "calculated" ("Berekend").
*
* @param currentSoilData The current soil parameter measurements for a field.
* @returns The enriched CurrentSoilData array.
*/
export function enrichCurrentSoilDataWithNlv(
currentSoilData: CurrentSoilData,
): CurrentSoilData {
const data = currentSoilData.filter(
(item) => item.parameter !== "d_n_supply_base" || item.value != null,
)
const hasNlv = data.some(
(item) => item.parameter === "d_n_supply_base" && item.value != null,
)
if (!hasNlv) {
const somItem = data.find((item) => item.parameter === "a_som_loi")
const clayItem = data.find((item) => item.parameter === "a_clay_mi")
const cnItem = data.find((item) => item.parameter === "a_cn_fr")
if (
somItem &&
typeof somItem.value === "number" &&
clayItem &&
typeof clayItem.value === "number" &&
cnItem &&
typeof cnItem.value === "number"
) {
const nlvVal =
Math.round(
calculateNlv({
a_clay_mi: clayItem.value,
a_cn_fr: cnItem.value,
a_som_loi: somItem.value,
}) * 10,
) / 10
data.push({
parameter: "d_n_supply_base",
value: nlvVal,
a_id: somItem.a_id,
b_sampling_date: somItem.b_sampling_date,
a_depth_upper: somItem.a_depth_upper,
a_depth_lower: somItem.a_depth_lower,
a_source: "calculated",
})
}
}
return data as CurrentSoilData
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
calculateNlvSupplyBySom,
calculateNlvSupplyIncreaseBySomPotential,
calculateWaterSupplyBySom,
getRegion,
getSoilParameterEstimates,
Expand Down Expand Up @@ -234,7 +234,7 @@ async function loadAsyncData(_calendar: string, latitude: number, longitude: num
}),
),
extraNMineralization: Math.round(
calculateNlvSupplyBySom({
calculateNlvSupplyIncreaseBySomPotential({
a_clay_mi: estimates.a_clay_mi,
a_cn_fr: estimates.a_cn_fr,
a_som_loi: estimates.a_som_loi,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,17 @@ export default function AtlasIndicatorsMap() {
const basePath = `/farm/${b_id_farm}/${calendar}/indicators`
const { capture } = useAnalytics()

useEffect(() => {
capture("atlas_viewed", { b_id_farm, calendar, layer: "indicators" })
}, [])

const [selectedProperty, setSelectedProperty] = useState("S_BLN")

useEffect(() => {
capture("atlas_viewed", {
b_id_farm,
calendar,
layer: "indicators",
property: selectedProperty,
})
}, [b_id_farm, calendar, selectedProperty, capture])

const selectedLabel =
selectedProperty === "avgScore"
? "Gemiddelde score"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { FeatureCollection, Geometry } from "geojson"
import type { MetaFunction } from "react-router"
import {
type CurrentSoilData,
getCurrentSoilDataForFarm,
getFields,
getSoilParametersDescription,
Expand Down Expand Up @@ -42,6 +41,7 @@ import { getCalendar, getTimeframe } from "~/lib/calendar"
import { clientConfig } from "~/lib/config"
import { handleLoaderError } from "~/lib/error"
import { fdm } from "~/lib/fdm.server"
import { enrichCurrentSoilDataWithNlv } from "~/lib/soil.server"
import { useSelectedAtlasSoilParameterStore } from "~/store/selected-soil-parameter"

export const meta: MetaFunction = () => {
Expand Down Expand Up @@ -95,16 +95,19 @@ export async function loader({ request, params }: LoaderFunctionArgs) {

const features = fields.map((field) => {
const fieldCurrentSoilData = currentSoilDataForFarm.get(field.b_id) ?? []
const fieldEnrichedSoilData = enrichCurrentSoilDataWithNlv(fieldCurrentSoilData)
const soilProps = fieldEnrichedSoilData.reduce(
(acc, data) => {
if (data.value !== null) acc[data.parameter] = data.value
return acc
},
{} as Record<string, string | number>,
)

const feature = {
type: "Feature" as const,
properties: {
...fieldCurrentSoilData.reduce(
(acc, data) => {
if (data.value !== null) acc[data.parameter] = data.value
return acc
},
{} as Record<CurrentSoilData[number]["parameter"], string | number>,
),
...soilProps,
b_id: field.b_id,
b_name: field.b_name,
b_area: Math.round((field.b_area ?? 0) * 10) / 10,
Expand Down Expand Up @@ -155,17 +158,22 @@ export default function FarmAtlasFieldSoilAnalysisBlock() {
const navigate = useNavigate()
const { capture } = useAnalytics()

useEffect(() => {
capture("atlas_viewed", { b_id_farm, calendar, layer: "soil_analysis" })
}, [])

const heatmapLayerId = "fieldsSavedHeatmap"
const heatmapOutlineLayerId = "fieldsSavedHeatmapOutline"
const selectedParameter = useSelectedAtlasSoilParameterStore((store) => store.selectedParameter)
const setSelectedParameter = useSelectedAtlasSoilParameterStore(
(store) => store.setSelectedParameter,
)

useEffect(() => {
capture("atlas_viewed", {
b_id_farm,
calendar,
layer: "soil_analysis",
parameter: selectedParameter,
})
}, [b_id_farm, calendar, selectedParameter, capture])

const [min, max] = useMemo(() => {
if (!fieldsData || fieldsData?.features.length === 0) {
return [0, 1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getSession } from "~/lib/auth.server"
import { getTimeframe } from "~/lib/calendar"
import { handleLoaderError } from "~/lib/error"
import { fdm } from "~/lib/fdm.server"
import { enrichCurrentSoilDataWithNlv } from "~/lib/soil.server"

/**
* Loader function for the soil data page of a specific farm field.
Expand Down Expand Up @@ -70,7 +71,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
start: null,
end: timeframe.end,
}),
getCurrentSoilData(fdm, session.principal_id, b_id, timeframe),
getCurrentSoilData(fdm, session.principal_id, b_id, timeframe).then(
enrichCurrentSoilDataWithNlv,
),
])

// Get soil parameter descriptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) {

// Get soil parameter descriptions and filter on the available soil parameters
const soilParameterDescription = getSoilParametersDescription().filter(
(item) =>
typeof soilAnalysis[item.parameter] !== "undefined" &&
soilAnalysis[item.parameter] !== null,
(item: { parameter: string }) =>
typeof (soilAnalysis as unknown as Record<string, unknown>)[item.parameter] !==
"undefined" &&
(soilAnalysis as unknown as Record<string, unknown>)[item.parameter] !== null,
)

// Return user information from loader
Expand Down
Loading
Loading