Matthias Nott
2 days ago fddc1891c9eb1d20a9d7f238fd34dfdb46b5e44d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#!/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.")