You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implement visual soil analysis in FDM based on the Dutch BodemConditieScore (BCS) methodology. This enables advisors to perform field-based visual soil assessments: photographing soil pits, annotating images with observations, scoring 9 BCS indicators, and sharing results with farmers — all from a mobile device.
Motivation
The BCS is a standardized, scientifically validated visual soil assessment method used in Dutch agriculture (paper)
Currently no mobile-friendly digital tool exists for BCS — assessors use paper forms
Visual soil analysis complements the existing lab-based soil analysis already in FDM
Advisors need to share annotated soil observations with farmers in an intuitive way
Integration with the Open Bodem Index (OBI) scoring system aligns with FDM's existing soil data infrastructure
BCS Methodology
9 Field-Observed Indicators (scored 0, 1, or 2)
Scoring scale: The database schema uses numericCasted to support half-points (0.5, 1.5) for future flexibility. In the current UI, only integer values (0, 1, 2) are offered to the user.
OBI Parameter
Dutch Name
English
Observation
A_SS_BCS
Bodemstructuur
Soil structure
Clod sizes, aggregate stability
A_SC_BCS
Verdichting ondergrond
Subsoil compaction
Resistance to penetration, plate structures
A_RD_BCS
Beworteling
Root depth
Root depth, density, branching
A_EW_BCS
Regenwormen
Earthworms
Earthworm count and species
A_CC_BCS
Gewasbedekking
Crop cover
% soil surface covered by vegetation
A_GS_BCS
Gekleurde vlekken
Gley spots
Rust/blue/grey spots (waterlogging)
A_P_BCS
Plasvorming
Ponding
Water puddles on surface
A_C_BCS
Scheuren
Cracks
Visible cracks in top layer
A_RT_BCS
Spoorvorming/vertrapping
Rutting/trampling
Wheel tracks, hoof damage
2 Derived Scores (from existing soil_analysis)
These are not entered separately — they are computed automatically from lab data already stored in the linked soil_analysis record (via soil_sampling):
Parameter
Source in soil_analysis
Calculation
bcs_om
a_som_loi + b_soiltype_agr + crop type
Triclass thresholds per soil/crop combination
bcs_ph
a_ph_cc → D_PH_DELTA (delta from optimum)
round(ind_ph(D_PH_DELTA) * 2) → 0/1/2
When a visual assessment is linked to the same soil_sampling record as a lab analysis, the bcs_om and bcs_ph scores are derived automatically. If no lab analysis exists for that sampling, D_BCS is calculated without these two components (partial score based on the 9 field-observed indicators only).
Three new tables in fdm-core, following the existing asset-action pattern:
erDiagram
fields ||--o{ soil_sampling : "b_id"
soil_sampling ||--o| soil_analysis : "a_id"
soil_sampling ||--o| soil_analysis_visual : "b_id_sampling"
soil_analysis_visual ||--o{ soil_analysis_visual_image : "a_id_visual"
soil_analysis_visual_image ||--o{ soil_analysis_visual_annotation : "a_id_image"
soil_analysis_visual {
text a_id_visual PK
text b_id_sampling FK
timestamp date
text assessor_name
text assessment_type "kuilmeting | bedrijfsmeting"
numeric a_ss_bcs "0-2"
numeric a_sc_bcs "0-2"
numeric a_rd_bcs "0-2"
numeric a_ew_bcs "0-2"
numeric a_cc_bcs "0-2"
numeric a_gs_bcs "0-2"
numeric a_p_bcs "0-2"
numeric a_c_bcs "0-2"
numeric a_rt_bcs "0-2"
numeric d_bcs "weighted total"
numeric i_bcs "0-1 normalized"
text notes
text weather_conditions
timestamp created
timestamp updated
}
soil_analysis_visual_image {
text a_id_image PK
text a_id_visual FK
text gcs_object_key
text image_type "profile | surface | roots | earthworms | structure | other"
integer sort_order
text caption
timestamp created
timestamp updated
}
soil_analysis_visual_annotation {
text a_id_annotation PK
text a_id_image FK
text type "pin | circle | arrow | freehand"
text data_json "Konva shape coordinates as percentages"
text text
text indicator "optional A_*_BCS link"
integer sort_order
timestamp created
timestamp updated
}
Loading
Key design decisions:
Links through soil_sampling to preserve spatial/temporal context — creates a new soil_sampling record if one doesn't exist
A visual assessment can accompany a lab analysis on the same sampling event
When a lab soil_analysis exists on the same sampling, bcs_om and bcs_ph are derived automatically; otherwise D_BCS is calculated without them
Annotations stored as JSON with percentage-based coordinates (responsive across devices)
Annotation types support pins, circles, arrows, and freehand — users can mark features beyond BCS
The indicator field is optional, allowing general observations not tied to a specific BCS indicator
Image Upload Architecture
Secure direct-to-GCS upload using V4 signed URLs:
sequenceDiagram
participant Browser
participant Server as React Router Server
participant GCS as Google Cloud Storage
Browser->>Server: POST /api/image-upload {farmId, contentType, size}
Server->>Server: checkPermission(farm, write)
Server->>GCS: generateSignedUrl(v4, write, 10min TTL)
Server-->>Browser: {uploadUrl, objectKey}
Browser->>GCS: PUT image (direct, no server relay)
GCS-->>Browser: 200 OK
Browser->>Server: POST /api/image-confirm {objectKey, metadata}
Server->>Server: validate + save to DB
Server-->>Browser: {success, a_id_image}
Loading
GCS object key structure:
farms/{b_id_farm}/visual-soil/{nanoid}.jpg
Security layers:
checkPermission('farm', 'write', b_id_farm) before issuing signed URL
Size limit enforced by GCS via X-Goog-Content-Length-Range header (10MB max)
Signed URL scoped to exact object path (10-min TTL)
Read access via short-lived signed read URLs (1-hour TTL)
Post-upload magic byte validation via file-type (already installed)
Image Annotation (react-konva)
Using react-konva for a canvas-based annotation UI with native touch support:
Annotation tools:
📍 Pin — tap to place a numbered marker with text note
Responsive layout: Full-width image viewer on mobile, side-by-side on tablet/desktop
Bottom sheet: Annotation text input via bottom sheet (not popup overlay) for mobile ergonomics
Authorization
Add "soil_analysis_visual" to the resources array in fdm-core/src/authorization.ts:
Role
View
Create/Edit
Delete
Owner
✅
✅
✅
Advisor
✅
✅
✅
Researcher
✅
❌
❌
User Flow
flowchart TD
A[Advisor opens field → Soil tab → Visueel] --> B[Tap 'Nieuwe visuele beoordeling']
B --> B2{Existing soil sampling?}
B2 -->|Yes| B3[Select or create new sampling]
B2 -->|No| B4[Create new soil_sampling record]
B3 --> C[Assessment form opens]
B4 --> C
C --> D{On smartphone?}
D -->|Yes| E[Tap 'Foto maken' → camera opens]
D -->|No| F[Drag & drop or browse files]
E --> G[Photo compressed & uploaded to GCS]
F --> G
G --> H[Image displayed on canvas]
H --> I[Select annotation tool: pin/circle/arrow/freehand]
I --> J[Tap/draw on image to annotate]
J --> K[Enter note + optionally link to BCS indicator]
K --> L{More annotations?}
L -->|Yes| I
L -->|No| M{More photos?}
M -->|Yes| E
M -->|No| N[Score the 9 BCS indicators: 0, 1, or 2]
N --> O[D_BCS calculated live + I_BCS shown]
O --> P[Save assessment]
P --> Q[Farmer sees: photos + annotations + scores in read-only view]
Loading
Implementation Plan
Phase 1: Data Model & Core (fdm-core)
Add 3 new tables + 4 enums to fdm-core/src/db/schema.ts
Add "soil_analysis_visual" resource to authorization.ts
Summary
Implement visual soil analysis in FDM based on the Dutch BodemConditieScore (BCS) methodology. This enables advisors to perform field-based visual soil assessments: photographing soil pits, annotating images with observations, scoring 9 BCS indicators, and sharing results with farmers — all from a mobile device.
Motivation
BCS Methodology
9 Field-Observed Indicators (scored 0, 1, or 2)
A_SS_BCSA_SC_BCSA_RD_BCSA_EW_BCSA_CC_BCSA_GS_BCSA_P_BCSA_C_BCSA_RT_BCS2 Derived Scores (from existing
soil_analysis)These are not entered separately — they are computed automatically from lab data already stored in the linked
soil_analysisrecord (viasoil_sampling):soil_analysisbcs_oma_som_loi+b_soiltype_agr+ crop typebcs_pha_ph_cc→D_PH_DELTA(delta from optimum)round(ind_ph(D_PH_DELTA) * 2)→ 0/1/2When a visual assessment is linked to the same
soil_samplingrecord as a lab analysis, thebcs_omandbcs_phscores are derived automatically. If no lab analysis exists for that sampling,D_BCSis calculated without these two components (partial score based on the 9 field-observed indicators only).BCS Score Calculation
Source: nmi-agro/Open-Bodem-Index-Calculator:R/bodemconditiescore.R
Technical Design
Data Model
Three new tables in
fdm-core, following the existing asset-action pattern:erDiagram fields ||--o{ soil_sampling : "b_id" soil_sampling ||--o| soil_analysis : "a_id" soil_sampling ||--o| soil_analysis_visual : "b_id_sampling" soil_analysis_visual ||--o{ soil_analysis_visual_image : "a_id_visual" soil_analysis_visual_image ||--o{ soil_analysis_visual_annotation : "a_id_image" soil_analysis_visual { text a_id_visual PK text b_id_sampling FK timestamp date text assessor_name text assessment_type "kuilmeting | bedrijfsmeting" numeric a_ss_bcs "0-2" numeric a_sc_bcs "0-2" numeric a_rd_bcs "0-2" numeric a_ew_bcs "0-2" numeric a_cc_bcs "0-2" numeric a_gs_bcs "0-2" numeric a_p_bcs "0-2" numeric a_c_bcs "0-2" numeric a_rt_bcs "0-2" numeric d_bcs "weighted total" numeric i_bcs "0-1 normalized" text notes text weather_conditions timestamp created timestamp updated } soil_analysis_visual_image { text a_id_image PK text a_id_visual FK text gcs_object_key text image_type "profile | surface | roots | earthworms | structure | other" integer sort_order text caption timestamp created timestamp updated } soil_analysis_visual_annotation { text a_id_annotation PK text a_id_image FK text type "pin | circle | arrow | freehand" text data_json "Konva shape coordinates as percentages" text text text indicator "optional A_*_BCS link" integer sort_order timestamp created timestamp updated }Key design decisions:
soil_samplingto preserve spatial/temporal context — creates a newsoil_samplingrecord if one doesn't existsoil_analysisexists on the same sampling,bcs_omandbcs_phare derived automatically; otherwise D_BCS is calculated without themindicatorfield is optional, allowing general observations not tied to a specific BCS indicatorImage Upload Architecture
Secure direct-to-GCS upload using V4 signed URLs:
sequenceDiagram participant Browser participant Server as React Router Server participant GCS as Google Cloud Storage Browser->>Server: POST /api/image-upload {farmId, contentType, size} Server->>Server: checkPermission(farm, write) Server->>GCS: generateSignedUrl(v4, write, 10min TTL) Server-->>Browser: {uploadUrl, objectKey} Browser->>GCS: PUT image (direct, no server relay) GCS-->>Browser: 200 OK Browser->>Server: POST /api/image-confirm {objectKey, metadata} Server->>Server: validate + save to DB Server-->>Browser: {success, a_id_image}GCS object key structure:
Security layers:
checkPermission('farm', 'write', b_id_farm)before issuing signed URLX-Goog-Content-Length-Rangeheader (10MB max)file-type(already installed)Image Annotation (react-konva)
Using
react-konvafor a canvas-based annotation UI with native touch support:Annotation tools:
Annotation data format (
data_json):All coordinates stored as percentages (0–100) — converted to pixels at render time based on current canvas size.
Mobile-First UX
<input type="file" accept="image/*" capture="environment">opens rear camera directly on mobilebrowser-image-compression)Authorization
Add
"soil_analysis_visual"to the resources array infdm-core/src/authorization.ts:User Flow
flowchart TD A[Advisor opens field → Soil tab → Visueel] --> B[Tap 'Nieuwe visuele beoordeling'] B --> B2{Existing soil sampling?} B2 -->|Yes| B3[Select or create new sampling] B2 -->|No| B4[Create new soil_sampling record] B3 --> C[Assessment form opens] B4 --> C C --> D{On smartphone?} D -->|Yes| E[Tap 'Foto maken' → camera opens] D -->|No| F[Drag & drop or browse files] E --> G[Photo compressed & uploaded to GCS] F --> G G --> H[Image displayed on canvas] H --> I[Select annotation tool: pin/circle/arrow/freehand] I --> J[Tap/draw on image to annotate] J --> K[Enter note + optionally link to BCS indicator] K --> L{More annotations?} L -->|Yes| I L -->|No| M{More photos?} M -->|Yes| E M -->|No| N[Score the 9 BCS indicators: 0, 1, or 2] N --> O[D_BCS calculated live + I_BCS shown] O --> P[Save assessment] P --> Q[Farmer sees: photos + annotations + scores in read-only view]Implementation Plan
Phase 1: Data Model & Core (
fdm-core)fdm-core/src/db/schema.ts"soil_analysis_visual"resource toauthorization.tsdrizzle-kit generateto create migrationfdm-core/src/soil-visual.ts:addVisualSoilAnalysis,getVisualSoilAnalysis,getVisualSoilAnalysesupdateVisualSoilAnalysis,removeVisualSoilAnalysisaddVisualSoilImage,removeVisualSoilImageaddImageAnnotation,updateImageAnnotation,removeImageAnnotationfdm-core/src/soil-visual.types.d.tsfdm-core/src/index.tsfdm-core/src/soil-visual.test.ts)Phase 2: Image Upload Infrastructure (
fdm-app)@google-cloud/storage,browser-image-compression,react-konva,konva,use-imageapi.image-upload.ts(generate signed upload URL)api.image-confirm.ts(validate + save image to DB)<ImageCapture>component (camera + dropzone for mobile/desktop)Phase 3: Visual Assessment UI (
fdm-app)soil._index.tsxsoil.visual._index.tsx(list visual assessments)soil.visual.new.tsx(new assessment)soil.visual.$a_id_visual.tsx(view/edit assessment)app/components/blocks/soil-visual/:<VisualAssessmentForm>— BCS scoring (9 indicators × 0-2 + computed D_BCS/I_BCS)<ImageGallery>— photo grid with upload<SoilAnnotator>— react-konva canvas with annotation tools<AnnotationToolbar>— mode selector (pin/circle/arrow/freehand)<AnnotationPopover>— text input + optional BCS indicator selector<BcsScoreCard>— score visualizationD_BCSformula) client-side for live previewPhase 4: Sharing & Polish
New Dependencies
@google-cloud/storagebrowser-image-compressionreact-konvakonvause-imageAlready available:
file-type,nanoid,zod,@remix-run/form-data-parser,@remix-run/file-storage