From a7814c1f4d0cbf6edd21425395b3d63748b799a3 Mon Sep 17 00:00:00 2001 From: luke11brown <13762210+luke11brown@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:24:08 +0400 Subject: [PATCH 1/2] Add workflow to generate Airfield Radar Holes Introduces a GitHub Actions workflow and supporting Python script to automatically generate Misc Other/Airfield_Radar_Holes.txt from OurAirports data. The workflow runs on schedule or relevant file changes, and creates a pull request with updated radar hole definitions for UK aerodromes, using runway geometry or fallback logic. --- .github/workflows/gen_radar_holes.yml | 57 +++++++++ workflows/build_airfield_radar_holes.py | 148 ++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 .github/workflows/gen_radar_holes.yml create mode 100644 workflows/build_airfield_radar_holes.py diff --git a/.github/workflows/gen_radar_holes.yml b/.github/workflows/gen_radar_holes.yml new file mode 100644 index 0000000000..b8f165f40e --- /dev/null +++ b/.github/workflows/gen_radar_holes.yml @@ -0,0 +1,57 @@ +name: Generate Airfield Radar Holes + +on: + schedule: + - cron: "0 3 */28 * *" # roughly every AIRAC; adjust if you prefer + push: + paths: + - "Airports/**" + - "workflows/build_airfield_radar_holes.py" + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Run radar hole generator + id: gen + env: + RECT_INFLATE_M: "400" # buffer around runway rectangle (m) + RUNWAY_BBOX_BUFFER_M: "250" # optional axis-aligned fallback buffer (m) + FALLBACK_HALF_SIDE_M: "1250" # square fallback half-side (m) + run: | + set -e + python workflows/build_airfield_radar_holes.py | tee gen.log + echo "summary<> $GITHUB_OUTPUT + head -n 100 gen.log >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + commit-message: "gen: Airfield_Radar_Holes.txt (P/S/C up to elev+10 ft)" + branch: automation/radar-holes + title: "Automation: update Airfield_Radar_Holes (elev+10 ft, rectangular coverage)" + labels: automation + add-paths: "Misc Other/Airfield_Radar_Holes.txt" + body: | + This PR updates **Misc Other/Airfield_Radar_Holes.txt**. + + - Top altitude = **elevation + 10 ft** for P/S/C + - Geometry = minimum-area rectangle around runway endpoints (+ buffer) + - Source = OurAirports (CC0) + - Generated by `workflows/build_airfield_radar_holes.py` + - Comments are ignored by the ESE compiler. + + ${{ steps.gen.outputs.summary }} diff --git a/workflows/build_airfield_radar_holes.py b/workflows/build_airfield_radar_holes.py new file mode 100644 index 0000000000..651b4050ad --- /dev/null +++ b/workflows/build_airfield_radar_holes.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +Generate Misc Other/Airfield_Radar_Holes.txt +------------------------------------------- +Creates radar HOLE definitions for all UK aerodromes (EG**) +based on public CC0 data from OurAirports. + +- Each HOLE inhibits Primary, Secondary, and Mode C up to (elevation + 10 ft) +- Rectangular polygons are derived from runway endpoints (inflated slightly) +- Falls back to a small square if no runway data is present +""" + +import csv, io, math, os, re, sys, urllib.request +from pathlib import Path +from typing import Dict, List, Tuple, Optional + +ROOT = Path(__file__).resolve().parents[1] +AIRPORTS_DIR = ROOT / "Airports" +OUTFILE = ROOT / "Misc Other" / "Airfield_Radar_Holes.txt" + +OURAIRPORTS_AIRPORTS = "https://ourairports.com/airports.csv" +OURAIRPORTS_RUNWAYS = "https://ourairports.com/runways.csv" + +# Parameters +RECT_INFLATE_M = float(os.getenv("RECT_INFLATE_M", 400.0)) +FALLBACK_HALF_SIDE_M = float(os.getenv("FALLBACK_HALF_SIDE_M", 1250.0)) + +def list_repo_icaos() -> List[str]: + return sorted( + d.name.upper() + for d in AIRPORTS_DIR.iterdir() + if d.is_dir() and re.fullmatch(r"EG[A-Z0-9]{2}", d.name.upper()) + ) + +def fetch_csv(url: str) -> List[Dict[str, str]]: + with urllib.request.urlopen(url, timeout=60) as r: + data = r.read().decode("utf-8", errors="replace") + return list(csv.DictReader(io.StringIO(data))) + +def dlat_dlon(lat_deg: float, dx_m: float, dy_m: float) -> Tuple[float, float]: + R = 6371008.8 + lat_r = math.radians(lat_deg) + dlat = dy_m / R + dlon = dx_m / (R * math.cos(lat_r)) + return math.degrees(dlat), math.degrees(dlon) + +def hem(val: float, latlon: str) -> str: + if latlon == "lat": return ("N" if val >= 0 else "S") + f"{abs(val):.6f}" + else: return ("E" if val >= 0 else "W") + f"{abs(val):.6f}" + +def square(lat: float, lon: float, half_side_m: float) -> List[Tuple[float,float]]: + dlat, _ = dlat_dlon(lat, 0, half_side_m) + _, dlon = dlat_dlon(lat, half_side_m, 0) + return [(lat-dlat, lon-dlon), (lat-dlat, lon+dlon), + (lat+dlat, lon+dlon), (lat+dlat, lon-dlon)] + +def convex_hull(points: List[Tuple[float,float]]) -> List[Tuple[float,float]]: + pts = sorted(set(points)) + if len(pts) <= 1: return pts + def cross(o,a,b): return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0]) + lower, upper = [], [] + for p in pts: + while len(lower)>=2 and cross(lower[-2],lower[-1],p)<=0: lower.pop() + lower.append(p) + for p in reversed(pts): + while len(upper)>=2 and cross(upper[-2],upper[-1],p)<=0: upper.pop() + upper.append(p) + return lower[:-1]+upper[:-1] + +def min_area_rect(points: List[Tuple[float,float]], inflate_m: float) -> List[Tuple[float,float]]: + import math + hull = convex_hull(points) + if len(hull) < 3: return [] + lat0 = sum(p[0] for p in hull)/len(hull) + lon0 = sum(p[1] for p in hull)/len(hull) + R = 6371008.8; cos0 = math.cos(math.radians(lat0)) + XY=[((math.radians(lo-lon0)*R*cos0),(math.radians(la-lat0)*R)) for la,lo in hull] + best=None + for i in range(len(XY)): + j=(i+1)%len(XY) + dx,dy=XY[j][0]-XY[i][0],XY[j][1]-XY[i][1] + ang=math.atan2(dy,dx) + rot=[(x*math.cos(-ang)-y*math.sin(-ang),x*math.sin(-ang)+y*math.cos(-ang)) for x,y in XY] + xs,ys=[p[0] for p in rot],[p[1] for p in rot] + minx,maxx,miny,maxy=min(xs),max(xs),min(ys),max(ys) + area=(maxx-minx)*(maxy-miny) + if best is None or area=2: + rect=min_area_rect(pts,RECT_INFLATE_M) + else: + rect=square(lat,lon,FALLBACK_HALF_SIDE_M) + if not rect: continue + fh.write(f"; {icao} — top {int(elev)} ft (ARP {lat:.6f}, {lon:.6f})\n") + fh.write(f"HOLE:{int(elev)}:{int(elev)}:{int(elev)}\n") + for la,lo in rect+[rect[0]]: + fh.write(f"COORD:{hem(la,'lat')}:{hem(lo,'lon')}\n") + fh.write("\n") + count+=1 + print(f"Wrote {count} HOLEs to {OUTFILE}") + +if __name__=="__main__": + main() From 65d851bbc4afb044db0bc746c2a85209aa10809f Mon Sep 17 00:00:00 2001 From: luke11brown <13762210+luke11brown@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:25:15 +0400 Subject: [PATCH 2/2] Update CHANGELOG.md --- .github/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 62a1ae4a79..7cb9de65c5 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -4,6 +4,7 @@ 3. AIRAC (2511) - Revised Luton (EGGW) engine runup area - thanks to @clc0609 (Coby Chapman) 4. AIRAC (2511) - Updated Shawbury (EGOS) runway designators - thanks to @ricky-gag38 (Riccardo Gagliardi) 5. Bug - Added EGSH land based fixes for HMRIs - thanks to @trevorhannant +6. Enhancement - Add radar holes to prevent display of ground aircraft at airfields - thanks to @luke11brown (Luke Brown) # Changes from release 2025/09 to 2025/10 1. AIRAC (2510) - Renamed Biggin Hill (EGKB) holding point L2 - thanks to @clc0609 (Coby Chapman)