#!/usr/bin/env python3 """Fix map images for Glidr question files: t30_q43, t30_q44, t30_q88, t60_q99.""" import urllib.request import urllib.error import os from PIL import Image, ImageDraw, ImageFont import math import io BASE = "/Users/i052341/Daten/Cloud/04 - Ablage/Ablage 2020 - 2029/Ablage 2025/Hobbies 2025/Segelflug/Theorie/Glidr" FIGURES_DIRS = [ os.path.join(BASE, "SPL Exam Questions FR", "figures"), os.path.join(BASE, "SPL Exam Questions EN", "figures"), os.path.join(BASE, "SPL Exam Questions DE", "figures"), ] WMS_TEMPLATE = ( "https://wms.geo.admin.ch/?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap" "&LAYERS=ch.bazl.luftfahrtkarten-icao&CRS=EPSG:4326" "&BBOX={lat_min},{lon_min},{lat_max},{lon_max}" "&WIDTH=1200&HEIGHT=900&FORMAT=image/png" ) IMG_W = 1200 IMG_H = 900 def fetch_wms(lat_min, lon_min, lat_max, lon_max): url = WMS_TEMPLATE.format(lat_min=lat_min, lon_min=lon_min, lat_max=lat_max, lon_max=lon_max) print(f" Fetching WMS: {url[:100]}...") req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urllib.request.urlopen(req, timeout=60) as resp: data = resp.read() img = Image.open(io.BytesIO(data)).convert("RGBA") return img def ll_to_px(lat, lon, lat_min, lon_min, lat_max, lon_max): px = (lon - lon_min) / (lon_max - lon_min) * IMG_W py = (1 - (lat - lat_min) / (lat_max - lat_min)) * IMG_H return (px, py) def draw_circle(draw, cx, cy, r=40, width=4, color="red"): draw.ellipse([cx - r, cy - r, cx + r, cy + r], outline=color, width=width) def draw_scale_bar(draw, img, lat_min, lon_min, lat_max, lon_max, scale_km, label_km, pos_x=60, pos_y=None): """Draw a scale bar at bottom-left.""" if pos_y is None: pos_y = IMG_H - 50 # Compute pixels per km at the center latitude center_lat = (lat_min + lat_max) / 2 lat_span = lat_max - lat_min lon_span = lon_max - lon_min # 1 degree lat ~ 111.32 km km_per_px_lat = (lat_span * 111.32) / IMG_H km_per_px_lon = (lon_span * 111.32 * math.cos(math.radians(center_lat))) / IMG_W km_per_px = (km_per_px_lat + km_per_px_lon) / 2 bar_px = int(scale_km / km_per_px) half = bar_px // 2 x0, x1 = pos_x, pos_x + bar_px y0, y1 = pos_y, pos_y + 14 # White background rectangle draw.rectangle([x0 - 4, y0 - 4, x1 + 4, y1 + 18], fill="white", outline="black", width=1) # Left half black, right half white with black border draw.rectangle([x0, y0, x0 + half, y1], fill="black", outline="black", width=1) draw.rectangle([x0 + half, y0, x1, y1], fill="white", outline="black", width=1) # Label try: font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 14) except Exception: font = ImageFont.load_default() draw.text((x0, y1 + 2), f"0", fill="black", font=font) draw.text((x0 + half - 8, y1 + 2), f"{label_km // 2}", fill="black", font=font) draw.text((x1 - 4, y1 + 2), f"{label_km} km", fill="black", font=font) def save_to_all(img, filename): for d in FIGURES_DIRS: out = os.path.join(d, filename) img.save(out, "PNG") print(f" Saved: {out}") # --------------------------------------------------------------------------- # ISSUE 1: t30_q43 — Birrfeld, wider bbox # --------------------------------------------------------------------------- def fix_q43(): print("\n=== t30_q43: Birrfeld airspace ===") lat_min, lat_max = 47.20, 47.70 lon_min, lon_max = 7.902, 8.569 img = fetch_wms(lat_min, lon_min, lat_max, lon_max) draw = ImageDraw.Draw(img) # Birrfeld LSZF: 47.4435, 8.2350 bx, by = ll_to_px(47.4435, 8.2350, lat_min, lon_min, lat_max, lon_max) draw_circle(draw, bx, by, r=40, width=4, color="red") # Scale bar 5 km draw_scale_bar(draw, img, lat_min, lon_min, lat_max, lon_max, scale_km=5, label_km=5, pos_x=60, pos_y=IMG_H - 60) save_to_all(img, "t30_q43.png") # --------------------------------------------------------------------------- # ISSUE 2: t30_q44 — Schwyz/Morgarten/Hinwil # --------------------------------------------------------------------------- def fix_q44(): print("\n=== t30_q44: Schwyz/Morgarten/Hinwil ===") lat_min, lat_max = 46.85, 47.45 lon_min, lon_max = 8.32, 9.12 img = fetch_wms(lat_min, lon_min, lat_max, lon_max) draw = ImageDraw.Draw(img) points = [ ("Schwyz", 47.02, 8.66), ("Morgarten", 47.10, 8.61), ("Hinwil", 47.30, 8.84), ] for name, lat, lon in points: px, py = ll_to_px(lat, lon, lat_min, lon_min, lat_max, lon_max) draw_circle(draw, px, py, r=40, width=4, color="red") # Scale bar 10 km draw_scale_bar(draw, img, lat_min, lon_min, lat_max, lon_max, scale_km=10, label_km=10, pos_x=60, pos_y=IMG_H - 60) save_to_all(img, "t30_q44.png") # --------------------------------------------------------------------------- # ISSUE 4: t30_q88 — Münster VS to Amsteg # --------------------------------------------------------------------------- def fix_q88(): print("\n=== t30_q88: Münster VS to Amsteg ===") lat_min, lat_max = 46.40, 46.86 lon_min, lon_max = 8.16, 8.78 img = fetch_wms(lat_min, lon_min, lat_max, lon_max) draw = ImageDraw.Draw(img) munster_lat, munster_lon = 46.486, 8.265 amsteg_lat, amsteg_lon = 46.776, 8.673 mx, my = ll_to_px(munster_lat, munster_lon, lat_min, lon_min, lat_max, lon_max) ax, ay = ll_to_px(amsteg_lat, amsteg_lon, lat_min, lon_min, lat_max, lon_max) # Red route line draw.line([(mx, my), (ax, ay)], fill="red", width=4) # Circles at both ends draw_circle(draw, mx, my, r=40, width=4, color="red") draw_circle(draw, ax, ay, r=40, width=4, color="red") # Scale bar 10 km draw_scale_bar(draw, img, lat_min, lon_min, lat_max, lon_max, scale_km=10, label_km=10, pos_x=60, pos_y=IMG_H - 60) save_to_all(img, "t30_q88.png") # --------------------------------------------------------------------------- # ISSUE 5: t60_q99 — Birrfeld-Courtelary-Grenchen route # --------------------------------------------------------------------------- def fix_q99(): print("\n=== t60_q99: Birrfeld-Courtelary-Grenchen ===") # lon range needed: 7.07 to 8.23 => span 1.16, but we use 1.4 for margin # lat span = 1.4 / 1.333 = 1.05, center lat 47.31 lat_min, lat_max = 46.78, 47.84 lon_min, lon_max = 7.00, 8.40 img = fetch_wms(lat_min, lon_min, lat_max, lon_max) draw = ImageDraw.Draw(img) birrfeld_lat, birrfeld_lon = 47.44, 8.23 courtelary_lat, courtelary_lon = 47.18, 7.07 grenchen_lat, grenchen_lon = 47.18, 7.39 bx, by = ll_to_px(birrfeld_lat, birrfeld_lon, lat_min, lon_min, lat_max, lon_max) cx, cy = ll_to_px(courtelary_lat, courtelary_lon, lat_min, lon_min, lat_max, lon_max) gx, gy = ll_to_px(grenchen_lat, grenchen_lon, lat_min, lon_min, lat_max, lon_max) # 2-segment red line: Birrfeld -> Courtelary -> Grenchen draw.line([(bx, by), (cx, cy)], fill="red", width=4) draw.line([(cx, cy), (gx, gy)], fill="red", width=4) # Circles at all three draw_circle(draw, bx, by, r=40, width=4, color="red") draw_circle(draw, cx, cy, r=40, width=4, color="red") draw_circle(draw, gx, gy, r=40, width=4, color="red") # Scale bar 15 km draw_scale_bar(draw, img, lat_min, lon_min, lat_max, lon_max, scale_km=15, label_km=15, pos_x=60, pos_y=IMG_H - 60) save_to_all(img, "t60_q99.png") if __name__ == "__main__": fix_q43() fix_q44() fix_q88() fix_q99() print("\nAll maps generated successfully.")