Skip to content

Add Visual Soil Analysis (BodemConditieScore) #613

@SvenVw

Description

@SvenVw

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

  • 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_ccD_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).

BCS Score Calculation

D_BCS = 2 × A_CC_BCS
      + 3 × A_RD_BCS
      + 3 × A_SC_BCS
      + 3 × A_EW_BCS
      + 3 × A_SS_BCS
      + 3 × bcs_ph
      + 3 × bcs_om
      + 1 × A_GS_BCS
      − 2 × A_P_BCS      ← negative contribution
      − 1 × A_C_BCS      ← negative contribution
      − 1 × A_RT_BCS     ← negative contribution

I_BCS = min(D_BCS / 40, 1.0)   # normalized 0–1 indicator

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
    }
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
  • Circle — draw circle around a soil feature
  • ➡️ Arrow — point at a specific observation
  • ✏️ Freehand — draw/circle freely around features

Annotation data format (data_json):

// Pin
{ "x": 45.2, "y": 62.8 }

// Circle
{ "cx": 50.0, "cy": 40.0, "radiusPercent": 12.5 }

// Arrow
{ "x1": 30.0, "y1": 20.0, "x2": 55.0, "y2": 45.0 }

// Freehand
{ "points": [10.2, 30.5, 12.1, 32.0, 14.5, 31.2] }

All coordinates stored as percentages (0–100) — converted to pixels at render time based on current canvas size.

Mobile-First UX

  • Camera capture: <input type="file" accept="image/*" capture="environment"> opens rear camera directly on mobile
  • Client-side compression: Compress images from 3–15MB to ~2MB before upload (Canvas API or browser-image-compression)
  • Touch annotation: Konva's Stage maps all touch events natively (touchstart → _pointerdown, etc.)
  • 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
  • Run drizzle-kit generate to create migration
  • Implement CRUD in fdm-core/src/soil-visual.ts:
    • addVisualSoilAnalysis, getVisualSoilAnalysis, getVisualSoilAnalyses
    • updateVisualSoilAnalysis, removeVisualSoilAnalysis
    • addVisualSoilImage, removeVisualSoilImage
    • addImageAnnotation, updateImageAnnotation, removeImageAnnotation
  • Define types in fdm-core/src/soil-visual.types.d.ts
  • Export from fdm-core/src/index.ts
  • Write integration tests (fdm-core/src/soil-visual.test.ts)

Phase 2: Image Upload Infrastructure (fdm-app)

  • Install packages: @google-cloud/storage, browser-image-compression, react-konva, konva, use-image
  • Create image upload integration module (signed URL generation)
  • Create API route: api.image-upload.ts (generate signed upload URL)
  • Create API route: api.image-confirm.ts (validate + save image to DB)
  • Create client-side upload utility with compression
  • Create <ImageCapture> component (camera + dropzone for mobile/desktop)

Phase 3: Visual Assessment UI (fdm-app)

  • Add "Visueel" tab to soil._index.tsx
  • Create route: soil.visual._index.tsx (list visual assessments)
  • Create route: soil.visual.new.tsx (new assessment)
  • Create route: soil.visual.$a_id_visual.tsx (view/edit assessment)
  • Create components in 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 visualization
  • Create Zod form schema for validation
  • Implement BCS score calculation (D_BCS formula) client-side for live preview

Phase 4: Sharing & Polish

  • Read-only view for farmers (photos + annotations + scores)
  • Image gallery with swipe navigation on mobile
  • Add column to fields table linking to latest visual soil analysis
  • Show visual soil analyses in soil page list alongside regular analyses

New Dependencies

Package Purpose Side
@google-cloud/storage GCS SDK for signed URL generation Server
browser-image-compression Client-side image compression Client
react-konva Canvas-based annotation with touch support Client
konva Canvas engine (peer dep of react-konva) Client
use-image Load images into Konva canvas Client

Already available: file-type, nanoid, zod, @remix-run/form-data-parser, @remix-run/file-storage

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions