- 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.
195 lines
6.6 KiB
Python
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")
|