from __future__ import annotations from typing import Any from .evidence import make_evidence from .http_client import get_json from .models import EvidenceItem, SiteSelection SOILGRIDS_URL = "https://rest.isric.org/soilgrids/v2.0/properties/query" def fetch_soil(selection: SiteSelection) -> tuple[dict[str, Any], list[EvidenceItem]]: if selection.anchor_lat is None or selection.anchor_lon is None: return {}, [ make_evidence( category="Soil / ground", finding="SoilGrids lookup was skipped because no real-world site coordinate is available.", source_name="SoilGrids / ISRIC", source_url="https://docs.isric.org/globaldata/soilgrids/", source_type="global soil model", resolution_or_scope="not available", confidence="low", limitation="Soil lookup needs a WGS84 coordinate.", design_implication="Use field/geotechnical verification before any soil-related design assumptions.", verification_needed="Provide a map/KML/GeoJSON boundary or anchor coordinate.", output_label="professional_verification_required", ) ] try: data = get_json( SOILGRIDS_URL, { "lon": selection.anchor_lon, "lat": selection.anchor_lat, "property": ["clay", "sand", "silt", "phh2o", "bdod", "soc"], "depth": "0-5cm", "value": "mean", }, timeout=30, ) values = _extract_soil_values(data) soil = _interpret_soil(values) return soil, [ make_evidence( category="Soil / ground", finding=_soil_finding(soil), source_name="SoilGrids / ISRIC", source_url="https://docs.isric.org/globaldata/soilgrids/", source_type="global soil model", resolution_or_scope="anchor coordinate; 0-5 cm modelled topsoil properties", confidence="low", limitation="SoilGrids is coarse global model data, not plot-level geotechnical testing or bearing-capacity evidence.", design_implication=soil.get("design_implication", "Use only as a ground-condition prompt."), verification_needed="Obtain site-specific geotechnical/professional verification before foundation or excavation decisions.", output_label="professional_verification_required", ) ] except Exception as exc: # noqa: BLE001 return {}, [ make_evidence( category="Soil / ground", finding="SoilGrids soil properties could not be retrieved.", source_name="SoilGrids / ISRIC", source_url="https://docs.isric.org/globaldata/soilgrids/", source_type="global soil model", resolution_or_scope="not available", confidence="low", limitation=f"SoilGrids request failed: {type(exc).__name__}.", design_implication="Do not infer soil type from this missing layer.", verification_needed="Use geotechnical report, soil test, local engineer input, or official soil maps.", output_label="professional_verification_required", ) ] def _extract_soil_values(data: dict[str, Any]) -> dict[str, float | None]: values: dict[str, float | None] = {} for layer in data.get("properties", {}).get("layers", []): name = layer.get("name") depths = layer.get("depths") or [] if not name or not depths: continue mean = (depths[0].get("values") or {}).get("mean") values[name] = float(mean) if mean is not None else None return values def _interpret_soil(values: dict[str, float | None]) -> dict[str, Any]: clay_pct = _gkg_to_pct(values.get("clay")) sand_pct = _gkg_to_pct(values.get("sand")) silt_pct = _gkg_to_pct(values.get("silt")) ph = _ph_value(values.get("phh2o")) texture = "mixed or uncertain texture" implication = "Use as a preliminary prompt for soil verification; do not make foundation decisions from this layer." if clay_pct is not None and clay_pct >= 35: texture = "clay-heavy topsoil signal" implication = "Check drainage, shrink-swell behaviour, water retention, and settlement risk with professional testing." elif sand_pct is not None and sand_pct >= 55: texture = "sandy topsoil signal" implication = "Check drainage, erosion, excavation stability, and bearing conditions with professional testing." elif silt_pct is not None and silt_pct >= 45: texture = "silt-heavy topsoil signal" implication = "Check waterlogging, compaction, erosion, and drainage behaviour with professional testing." return { "texture_signal": texture, "clay_pct": clay_pct, "sand_pct": sand_pct, "silt_pct": silt_pct, "ph_h2o": ph, "bulk_density_raw": values.get("bdod"), "soc_raw": values.get("soc"), "design_implication": implication, "safe_note": "Preliminary SoilGrids model output only; verify with geotechnical/professional sources.", } def _soil_finding(soil: dict[str, Any]) -> str: parts = [soil.get("texture_signal") or "soil texture signal unavailable"] if soil.get("clay_pct") is not None: parts.append(f"clay {soil['clay_pct']}%") if soil.get("sand_pct") is not None: parts.append(f"sand {soil['sand_pct']}%") if soil.get("silt_pct") is not None: parts.append(f"silt {soil['silt_pct']}%") if soil.get("ph_h2o") is not None: parts.append(f"pH {soil['ph_h2o']}") return "SoilGrids preliminary topsoil signal: " + ", ".join(parts) + "." def _gkg_to_pct(value: float | None) -> float | None: return round(value / 10, 1) if value is not None else None def _ph_value(value: float | None) -> float | None: return round(value / 10, 1) if value is not None else None