"""
Soil Quality Assessment App
Methodology based on:
Luján Soto, R. (2018). Cuaderno de campo para la evaluación de prácticas
regenerativas sobre la calidad del suelo.
Universidad de Córdoba – CEBAS-CSIC.
Educational inspiration:
Ecorestauración – https://ecorestauracion.es/curso/
"""
import re
import os
import datetime
import tempfile
import pandas as pd
import gradio as gr
# =========================
# PDF (reportlab)
# =========================
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
from reportlab.platypus import Image as RLImage
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
# =========================
# CONFIG
# =========================
XLSX_PATH = "Soil Anaylsis.xlsx"
IND_SHEET = "Indicators"
OVERALL_SHEET = "Overall Score"
INDICATOR_IMAGE_EXT = ".png" # 1.png, 2.png, ...
SEASON_COLS = {
"Fall": "Available in Fall",
"Winter": "Available in Winter",
"Spring": "Available in Spring",
"Summer": "Available in summer",
}
SCORE_MAP = {"Low": 1, "Medium": 2, "High": 3}
# =========================
# LOAD DATA
# =========================
if not os.path.exists(XLSX_PATH):
raise FileNotFoundError(f"Missing file: {XLSX_PATH}")
ind_df = pd.read_excel(XLSX_PATH, sheet_name=IND_SHEET)
# =========================
# HELPERS
# =========================
def normalize_text(s):
if s is None or (isinstance(s, float) and pd.isna(s)):
return ""
return str(s).strip()
def is_available(row, season):
return normalize_text(row.get(SEASON_COLS[season], "")).lower() == "x"
def build_season_indicator_list(season):
return [r for _, r in ind_df.iterrows() if is_available(r, season)]
def get_indicator_image(indicator_id):
path = f"{indicator_id}{INDICATOR_IMAGE_EXT}"
return path if os.path.exists(path) else None
def save_user_image(image):
if image is None:
return None
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
image.save(tmp.name)
tmp.close()
return tmp.name
def indicator_prompt(row, idx, total):
return f"""
### Indicator {idx + 1} / {total}
**{int(row['Indicator ID'])}. {row['Indicator Name']}**
**Data entry:** {normalize_text(row.get('Data entry',''))}
**Motivation**
{normalize_text(row.get('Motivation',''))}
**Guide**
{normalize_text(row.get('Guide',''))}
"""
def soil_quality_label(total):
if 15 <= total <= 25:
return "Low soil quality"
if 26 <= total <= 37:
return "Medium soil quality"
if 38 <= total <= 48:
return "High soil quality"
return "Outside expected range"
# =========================
# PDF REPORT
# =========================
def generate_pdf_report(state):
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
doc = SimpleDocTemplate(tmp.name, pagesize=A4)
styles = getSampleStyleSheet()
story = []
story.append(Paragraph("Soil Quality Assessment Report", styles["Title"]))
story.append(Spacer(1, 12))
story.append(Paragraph(f"Season: {state['season']}", styles["Normal"]))
story.append(Paragraph(f"Location: {state['location']}", styles["Normal"]))
story.append(Paragraph(f"Date: {datetime.date.today().isoformat()}", styles["Normal"]))
story.append(Spacer(1, 20))
total_score = 0
for idx in sorted(state["results"].keys()):
r = state["results"][idx]
total_score += r["score"]
story.append(Paragraph(
f"{r['id']}. {r['name']}",
styles["Heading2"]
))
story.append(Spacer(1, 6))
if r.get("ref_image") and os.path.exists(r["ref_image"]):
story.append(Paragraph("Reference image", styles["Normal"]))
story.append(RLImage(r["ref_image"], width=8*cm, height=6*cm))
story.append(Spacer(1, 6))
if r.get("user_image") and os.path.exists(r["user_image"]):
story.append(Paragraph("User photo", styles["Normal"]))
story.append(RLImage(r["user_image"], width=8*cm, height=6*cm))
story.append(Spacer(1, 6))
story.append(Paragraph(
f"Assessment: {r['assessment']} ({r['score']} points)",
styles["Normal"]
))
story.append(Spacer(1, 6))
story.append(Paragraph(
f"Motivation:
{r['motivation']}",
styles["Normal"]
))
story.append(Spacer(1, 6))
if r.get("comment"):
story.append(Paragraph(
f"User observation:
{r['comment']}",
styles["Normal"]
))
story.append(Spacer(1, 12))
story.append(PageBreak())
story.append(Paragraph("Overall Result", styles["Heading1"]))
story.append(Spacer(1, 12))
story.append(Paragraph(f"Total score: {total_score} / 48", styles["Normal"]))
story.append(Paragraph(
f"Soil quality classification: {soil_quality_label(total_score)}",
styles["Normal"]
))
story.append(Spacer(1, 12))
story.append(Paragraph(
"Score interpretation:
"
"• 15 – 25 → Low soil quality
"
"• 26 – 37 → Medium soil quality
"
"• 38 – 48 → High soil quality",
styles["Normal"]
))
story.append(PageBreak())
story.append(Paragraph("Credits & Acknowledgements", styles["Heading1"]))
story.append(Spacer(1, 12))
story.append(Paragraph(
"""
This assessment tool is based on the guide:
Luján Soto, Raquel (2018).
Cuaderno de campo para la evaluación de prácticas regenerativas sobre la calidad del suelo.
Instituto de Sociología y Estudios Campesinos (Universidad de Córdoba)
Centro de Edafología y Biología Aplicada del Segura (CEBAS–CSIC).
The guide forms part of the doctoral research project
“Investigación Acción Participativa en Agricultura Regenerativa”,
supported by Fundación Bancaria “la Caixa”
(Grant LCF/BQ/ES17/11600008).
This report and application are also inspired by the training programme on
regenerative agriculture and ecological restoration developed by
Ecorestauración.
""",
styles["Normal"]
))
doc.build(story)
tmp.close()
return tmp.name
# =========================
# STATE & NAVIGATION
# =========================
def load_indicator(state):
idx = state["idx"]
row = state["indicators"][idx]
saved = state["results"].get(idx, {})
indicator_id = int(row["Indicator ID"])
ref_image = get_indicator_image(indicator_id)
data_entry = normalize_text(row.get("Data entry", "")).lower()
return (
indicator_prompt(row, idx, len(state["indicators"])),
ref_image,
saved.get("assessment", "Medium"),
saved.get("number"),
saved.get("comment", ""),
state
)
def save_current(image, number, text, assessment, state):
row = state["indicators"][state["idx"]]
indicator_id = int(row["Indicator ID"])
state["results"][state["idx"]] = {
"id": indicator_id,
"name": normalize_text(row["Indicator Name"]),
"assessment": assessment,
"score": SCORE_MAP[assessment],
"number": number,
"comment": text,
"motivation": normalize_text(row.get("Motivation", "")),
"ref_image": get_indicator_image(indicator_id),
"user_image": save_user_image(image),
}
def start_session(season, location):
indicators = build_season_indicator_list(season)
state = {
"season": season,
"location": location,
"idx": 0,
"indicators": indicators,
"results": {}
}
return (*load_indicator(state), gr.update(visible=True), gr.update(visible=False))
def go_next(image, number, text, assessment, state):
save_current(image, number, text, assessment, state)
if state["idx"] < len(state["indicators"]) - 1:
state["idx"] += 1
return (*load_indicator(state), gr.update(visible=True), gr.update(visible=False))
pdf = generate_pdf_report(state)
return (
"✅ Assessment completed.",
None,
None,
None,
None,
state,
gr.update(visible=False),
gr.update(value=pdf, visible=True)
)
def go_prev(state):
if state["idx"] > 0:
state["idx"] -= 1
return (*load_indicator(state), state)
# =========================
# UI
# =========================
with gr.Blocks(title="Soil Quality Assessment") as demo:
gr.Markdown("# 🌱 Soil Quality Assessment")
state = gr.State({})
season = gr.Dropdown(["Fall", "Winter", "Spring", "Summer"], value="Spring")
location = gr.Textbox(label="Location")
start_btn = gr.Button("Start assessment")
indicator_md = gr.Markdown()
reference_image = gr.Image(label="Reference image", interactive=False)
image_in = gr.Image(type="pil", label="Field photo")
number_in = gr.Number(label="Numeric value")
text_in = gr.Textbox(label="Observation")
chosen = gr.Radio(["Low", "Medium", "High"], value="Medium")
with gr.Row():
prev_btn = gr.Button("⬅ Previous")
next_btn = gr.Button("Next ➡", visible=False)
pdf_out = gr.File(label="📄 Download assessment report", visible=False)
start_btn.click(
start_session,
inputs=[season, location],
outputs=[indicator_md, reference_image, chosen, number_in, text_in, state, next_btn, pdf_out]
)
next_btn.click(
go_next,
inputs=[image_in, number_in, text_in, chosen, state],
outputs=[indicator_md, reference_image, chosen, number_in, text_in, state, next_btn, pdf_out]
)
prev_btn.click(
go_prev,
inputs=[state],
outputs=[indicator_md, reference_image, chosen, number_in, text_in, state]
)
gr.Markdown(
"""
---
### 📚 Credits & Acknowledgements
This application is based on the methodological framework developed in:
**Luján Soto, Raquel (2018)**
*Cuaderno de campo para la evaluación de prácticas regenerativas sobre la calidad del suelo.*
Instituto de Sociología y Estudios Campesinos (Universidad de Córdoba)
Centro de Edafología y Biología Aplicada del Segura (CEBAS–CSIC)
The guide was developed within the doctoral research project
*“Investigación Acción Participativa en Agricultura Regenerativa”*,
supported by Fundación Bancaria “la Caixa”
(Grant **LCF/BQ/ES17/11600008**).
This tool is also inspired by the educational approach and materials of
**Ecorestauración**
https://ecorestauracion.es/curso/
"""
)
demo.launch()