|
| 1 | +#!/usr/bin/env python |
| 2 | +"""Compare the nominal recovery zone for a single main vs dual deployment. |
| 3 | +
|
| 4 | +Reads the nominal landing footprints from the single-main run (output/nominal) |
| 5 | +and the dual-deploy run (output-dualdeploy) and draws both on the launch-site map. |
| 6 | +
|
| 7 | +Run via uv after both trochia runs: uv run recovery-map.py # -> recovery-map.png |
| 8 | +""" |
| 9 | +import csv |
| 10 | +import os |
| 11 | + |
| 12 | +import pymap3d as pm |
| 13 | +from PIL import ImageDraw, ImageFont |
| 14 | +from staticmap import StaticMap, Line, CircleMarker |
| 15 | + |
| 16 | +LAUNCH_LAT, LAUNCH_LON, LAUNCH_ALT = 43.7961, -120.6518, 1390.0 |
| 17 | + |
| 18 | +ZONES = [ |
| 19 | + ("output/nominal/89/ghp.csv", "single main (~10 km)", "#cc0000"), |
| 20 | + ("output-dualdeploy/89/ghp.csv", "dual deploy (~3 km)", "#31688e"), |
| 21 | +] |
| 22 | + |
| 23 | + |
| 24 | +def enu_to_lonlat(e, n): |
| 25 | + x, y, z = pm.enu2ecef(e, n, 0.0, LAUNCH_LAT, LAUNCH_LON, LAUNCH_ALT) |
| 26 | + lat, lon, _ = pm.ecef2geodetic(x, y, z) |
| 27 | + return lon, lat |
| 28 | + |
| 29 | + |
| 30 | +def loop(path): |
| 31 | + pts = [] |
| 32 | + with open(path) as f: |
| 33 | + for row in csv.DictReader(f, skipinitialspace=True): |
| 34 | + pts.append(enu_to_lonlat(float(row["ghp_e"]), float(row["ghp_n"]))) |
| 35 | + return pts |
| 36 | + |
| 37 | + |
| 38 | +def find_font(size): |
| 39 | + for p in ("/usr/share/fonts/TTF/Roboto-Regular.ttf", |
| 40 | + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"): |
| 41 | + try: |
| 42 | + return ImageFont.truetype(p, size) |
| 43 | + except OSError: |
| 44 | + continue |
| 45 | + return ImageFont.load_default() |
| 46 | + |
| 47 | + |
| 48 | +def main(): |
| 49 | + m = StaticMap(900, 900, url_template="https://tile.openstreetmap.org/{z}/{x}/{y}.png") |
| 50 | + for path, _, color in ZONES: |
| 51 | + m.add_line(Line(loop(path), color, 3)) |
| 52 | + m.add_marker(CircleMarker(enu_to_lonlat(0.0, 0.0), "black", 13)) |
| 53 | + m.add_marker(CircleMarker(enu_to_lonlat(0.0, 0.0), "white", 7)) |
| 54 | + |
| 55 | + img = m.render().convert("RGB") |
| 56 | + d = ImageDraw.Draw(img, "RGBA") |
| 57 | + font, title = find_font(15), find_font(16) |
| 58 | + w, h = 230, 24 * (len(ZONES) + 1) + 34 |
| 59 | + d.rectangle([10, 10, 10 + w, 10 + h], fill=(255, 255, 255, 225), outline=(0, 0, 0, 255)) |
| 60 | + d.text((20, 18), "nominal recovery zone", font=title, fill=(0, 0, 0)) |
| 61 | + for i, (_, label, color) in enumerate(ZONES): |
| 62 | + y = 44 + i * 24 |
| 63 | + d.line([(20, y + 8), (50, y + 8)], fill=color, width=4) |
| 64 | + d.text((58, y), label, font=font, fill=(0, 0, 0)) |
| 65 | + y = 44 + len(ZONES) * 24 |
| 66 | + d.ellipse([30, y + 2, 40, y + 12], fill="black") |
| 67 | + d.text((58, y), "launch (Brothers, OR)", font=font, fill=(0, 0, 0)) |
| 68 | + |
| 69 | + img.save("recovery-map.png") |
| 70 | + print("saved recovery-map.png") |
| 71 | + |
| 72 | + |
| 73 | +if __name__ == "__main__": |
| 74 | + main() |
0 commit comments