SmartCane/create_field_checklist.py
Timon 711a005e52 Enhance functionality and maintainability across multiple scripts
- Updated settings.local.json to include new read permissions for r_app directory.
- Adjusted the harvest readiness condition in 80_utils_cane_supply.R to lower the imminent probability threshold.
- Removed unused get_status_trigger function from 80_utils_common.R to streamline code.
- Added total area analyzed feature in 90_CI_report_with_kpis_agronomic_support.Rmd, including area calculations in summary tables.
- Updated translations_90.json to include new keys for total area analyzed and area label.
- Created create_field_checklist.R and create_field_checklist.py scripts to generate Excel checklists from GeoJSON data, sorting fields by area and splitting assignments among team members.
2026-03-18 14:17:54 +01:00

195 lines
6.6 KiB
Python

"""
Creates an Excel checklist from pivot.geojson, with fields sorted largest to smallest,
split across Timon (1st), Joey (2nd), Dimitra (3rd) in a single sheet side-by-side.
"""
import json
import math
import os
try:
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
except ImportError:
print("Installing openpyxl...")
import subprocess, sys
subprocess.check_call([sys.executable, "-m", "pip", "install", "openpyxl"])
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
def shoelace_area(coords):
"""Compute area in square degrees using the shoelace formula."""
n = len(coords)
area = 0.0
for i in range(n):
j = (i + 1) % n
area += coords[i][0] * coords[j][1]
area -= coords[j][0] * coords[i][1]
return abs(area) / 2.0
def polygon_area_m2(ring):
"""
Approximate polygon area in m² using spherical coordinates.
ring: list of [lon, lat] pairs
"""
R = 6371000 # Earth radius in meters
area_deg2 = shoelace_area(ring)
# Convert to m² using mean latitude
mean_lat = sum(p[1] for p in ring) / len(ring)
lat_rad = math.radians(mean_lat)
# 1 degree lat ≈ R * pi/180 meters, 1 degree lon ≈ R * cos(lat) * pi/180 meters
m_per_deg_lat = R * math.pi / 180
m_per_deg_lon = R * math.cos(lat_rad) * math.pi / 180
return area_deg2 * m_per_deg_lat * m_per_deg_lon
def feature_area(feature):
"""Compute total area of a feature (MultiPolygon or Polygon) in m²."""
geom = feature["geometry"]
total = 0.0
if geom["type"] == "MultiPolygon":
for polygon in geom["coordinates"]:
# First ring is outer boundary
total += polygon_area_m2(polygon[0])
elif geom["type"] == "Polygon":
total += polygon_area_m2(geom["coordinates"][0])
return total
# Load GeoJSON
geojson_path = r"C:\Users\timon\Documents\SmartCane_code\laravel_app\storage\app\angata\pivot.geojson"
with open(geojson_path, "r", encoding="utf-8") as f:
gj = json.load(f)
features = gj["features"]
print(f"Total features: {len(features)}")
# Compute areas and sort
fields = []
for feat in features:
field_name = feat["properties"].get("field", "?")
area = feature_area(feat)
fields.append({"field": field_name, "area_m2": area, "area_ha": area / 10000})
fields.sort(key=lambda x: x["area_m2"], reverse=True)
# Print top 10 for verification
print("\nTop 10 fields by area:")
for i, f in enumerate(fields[:10]):
print(f" {i+1:3d}. Field {f['field']:15s} {f['area_ha']:.2f} ha")
# Split: index 0,3,6,... → Timon; 1,4,7,... → Joey; 2,5,8,... → Dimitra
timon = [f for i, f in enumerate(fields) if i % 3 == 0]
joey = [f for i, f in enumerate(fields) if i % 3 == 1]
dimitra = [f for i, f in enumerate(fields) if i % 3 == 2]
print(f"\nSplit: Timon={len(timon)}, Joey={len(joey)}, Dimitra={len(dimitra)}")
# Create Excel
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Field Checklist"
# Color palette
colors = {
"timon": {"header_bg": "1F6AA5", "alt_bg": "D6E4F0"},
"joey": {"header_bg": "2E7D32", "alt_bg": "D7F0D8"},
"dimitra": {"header_bg": "7B1FA2", "alt_bg": "EDD7F0"},
}
white_font = Font(name="Calibri", bold=True, color="FFFFFF", size=11)
black_font = Font(name="Calibri", size=10)
bold_black = Font(name="Calibri", bold=True, size=10)
center_align = Alignment(horizontal="center", vertical="center", wrap_text=True)
thin = Side(style="thin", color="CCCCCC")
border = Border(left=thin, right=thin, top=thin, bottom=thin)
# Column layout: each person gets 3 columns (Rank, Field, Area ha, Checked)
# Spacing: col 1-4 = Timon, col 5 = spacer, col 6-9 = Joey, col 10 = spacer, col 11-14 = Dimitra
persons = [
("Timon", timon, 1),
("Joey", joey, 6),
("Dimitra", dimitra, 11),
]
# Row 1: Person name header (merged across 4 cols)
ws.row_dimensions[1].height = 25
ws.row_dimensions[2].height = 20
for name, data, start_col in persons:
c = colors[name.lower()]
# Merge cells for person name
end_col = start_col + 3
ws.merge_cells(
start_row=1, start_column=start_col,
end_row=1, end_column=end_col
)
cell = ws.cell(row=1, column=start_col, value=name)
cell.font = white_font
cell.fill = PatternFill("solid", fgColor=c["header_bg"])
cell.alignment = center_align
cell.border = border
# Sub-headers
sub_headers = ["#", "Field", "Area (ha)", "Checked ✓"]
for i, hdr in enumerate(sub_headers):
cell = ws.cell(row=2, column=start_col + i, value=hdr)
cell.font = white_font
cell.fill = PatternFill("solid", fgColor=c["header_bg"])
cell.alignment = center_align
cell.border = border
# Data rows
for row_i, field in enumerate(data):
row_num = row_i + 3
ws.row_dimensions[row_num].height = 16
alt = row_i % 2 == 1
bg = c["alt_bg"] if alt else "FFFFFF"
fill = PatternFill("solid", fgColor=bg)
values = [row_i + 1, field["field"], round(field["area_ha"], 2), ""]
for col_i, val in enumerate(values):
cell = ws.cell(row=row_num, column=start_col + col_i, value=val)
cell.font = black_font
cell.fill = fill
cell.border = border
cell.alignment = Alignment(horizontal="center" if col_i != 1 else "left",
vertical="center")
# Column widths
col_widths = {1: 5, 2: 14, 3: 10, 4: 12, # Timon
5: 2, # spacer
6: 5, 7: 14, 8: 10, 9: 12, # Joey
10: 2, # spacer
11: 5, 12: 14, 13: 10, 14: 12} # Dimitra
for col, width in col_widths.items():
ws.column_dimensions[get_column_letter(col)].width = width
# Freeze header rows
ws.freeze_panes = "A3"
# Title row above everything — insert a title row
ws.insert_rows(1)
ws.merge_cells("A1:N1")
title_cell = ws.cell(row=1, column=1, value="Angata Pivot Field Checklist — sorted largest to smallest")
title_cell.font = Font(name="Calibri", bold=True, size=13, color="1A1A1A")
title_cell.alignment = Alignment(horizontal="center", vertical="center")
title_cell.fill = PatternFill("solid", fgColor="F0F0F0")
ws.row_dimensions[1].height = 28
# Re-freeze after insert
ws.freeze_panes = "A4"
out_path = r"C:\Users\timon\Documents\SmartCane_code\angata_field_checklist.xlsx"
wb.save(out_path)
print(f"\nExcel saved to: {out_path}")
print(f"Total fields: {len(fields)}")
print(f" Timon: {len(timon)} fields")
print(f" Joey: {len(joey)} fields")
print(f" Dimitra: {len(dimitra)} fields")