#!/usr/bin/env python3 """Generate solar/time-zone metadata for existing Agrarian terrain tiles. The script intentionally skips placeholder/unknown tiles. That keeps the Earth-scale path cheap: solar metadata is generated only after a tile has real source work attached. """ from __future__ import annotations import json import math from datetime import datetime, timezone from pathlib import Path ROOT = Path(__file__).resolve().parents[1] REGISTRY_PATH = ROOT / "Data" / "Tiles" / "ground_zero_tiles.json" OUTPUT_PATH = ROOT / "Data" / "Tiles" / "tile_solar_metadata.json" SOLAR_READY_STATUSES = {"source_data_found", "generated", "validated", "packaged", "published"} TIMEZONE_OVERRIDES = { "gz_us_ca_pacifica_utm10n_e544_n4160": { "time_zone_id": "America/Los_Angeles", "standard_utc_offset_hours": -8.0, "daylight_utc_offset_hours": -7.0, } } def solar_hours(latitude: float, longitude: float, utc_offset_hours: float, day_of_year: int) -> dict: gamma = (2.0 * math.pi / 365.0) * (day_of_year - 1) equation_of_time = 229.18 * ( 0.000075 + 0.001868 * math.cos(gamma) - 0.032077 * math.sin(gamma) - 0.014615 * math.cos(2.0 * gamma) - 0.040849 * math.sin(2.0 * gamma) ) declination = ( 0.006918 - 0.399912 * math.cos(gamma) + 0.070257 * math.sin(gamma) - 0.006758 * math.cos(2.0 * gamma) + 0.000907 * math.sin(2.0 * gamma) - 0.002697 * math.cos(3.0 * gamma) + 0.00148 * math.sin(3.0 * gamma) ) lat_rad = math.radians(max(-89.8, min(89.8, latitude))) zenith_rad = math.radians(90.833) hour_angle_arg = (math.cos(zenith_rad) / (math.cos(lat_rad) * math.cos(declination))) - ( math.tan(lat_rad) * math.tan(declination) ) solar_noon = (720.0 - (4.0 * longitude) - equation_of_time + (utc_offset_hours * 60.0)) / 60.0 solar_noon %= 24.0 if hour_angle_arg <= -1.0: return {"sunrise_hour": 0.0, "sunset_hour": 24.0, "solar_noon_hour": round(solar_noon, 3), "day_length_hours": 24.0} if hour_angle_arg >= 1.0: return {"sunrise_hour": round(solar_noon, 3), "sunset_hour": round(solar_noon, 3), "solar_noon_hour": round(solar_noon, 3), "day_length_hours": 0.0} hour_angle_deg = math.degrees(math.acos(hour_angle_arg)) sunrise = ((solar_noon * 60.0) - (hour_angle_deg * 4.0)) / 60.0 sunset = ((solar_noon * 60.0) + (hour_angle_deg * 4.0)) / 60.0 return { "sunrise_hour": round(sunrise % 24.0, 3), "sunset_hour": round(sunset % 24.0, 3), "solar_noon_hour": round(solar_noon, 3), "day_length_hours": round((sunset - sunrise), 3), } def main() -> None: registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) records = [] for tile in registry.get("tiles", []): status = tile.get("status") tile_id = tile.get("tile_id") grid = tile.get("grid", {}) if status not in SOLAR_READY_STATUSES or not tile_id or tile_id not in TIMEZONE_OVERRIDES: continue timezone_data = TIMEZONE_OVERRIDES[tile_id] latitude = float(grid["center_latitude"]) longitude = float(grid["center_longitude"]) utc_offset = float(timezone_data["daylight_utc_offset_hours"]) sample_days = { "march_equinox": 80, "june_solstice": 172, "september_equinox": 266, "december_solstice": 355, } records.append( { "tile_id": tile_id, "center_latitude": latitude, "center_longitude": longitude, **timezone_data, "solar_model": "NOAA approximate sunrise/sunset", "sample_solar_hours": { label: solar_hours(latitude, longitude, utc_offset, day) for label, day in sample_days.items() }, } ) OUTPUT_PATH.write_text( json.dumps( { "schema_version": 1, "generated_at_utc": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), "source_registry": str(REGISTRY_PATH.relative_to(ROOT)), "generation_rule": "Only tiles with real source status and explicit timezone data are emitted.", "tiles": records, }, indent=2, ) + "\n", encoding="utf-8", ) print(f"Wrote {len(records)} tile solar metadata record(s) to {OUTPUT_PATH.relative_to(ROOT)}") if __name__ == "__main__": main()