131 lines
4.5 KiB
Python
131 lines
4.5 KiB
Python
#!/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()
|