diff --git a/mapbuilder/builder.py b/mapbuilder/builder.py index 71af842..ba21225 100644 --- a/mapbuilder/builder.py +++ b/mapbuilder/builder.py @@ -5,6 +5,7 @@ from .cache import Cache from .data.aixm2 import parse_aixm from .data.kml import KMLParser +from .data.rwy import parse_runway from .data.sidstar import parse_sidstar from .dfs import aixm from .handlers.jinja import JinjaHandler @@ -34,7 +35,9 @@ def __init__(self, source_dir, target_dir, cache_dir, config): data_root = None if "root" in config["data"][data_source]: data_root = config["data"][data_source]["root"] - parser = KMLParser(source_dir / config["data"][data_source]["source"], data_root) + parser = KMLParser( + source_dir / config["data"][data_source]["source"], data_root + ) self.data[data_source] = parser.parse() elif data_source_type == "raw": logging.debug(f"Loading raw source {data_source}...") @@ -45,7 +48,16 @@ def __init__(self, source_dir, target_dir, cache_dir, config): elif data_source_type == "ese": logging.debug(f"Loading ESE source {data_source}...") self.data[data_source] = { - "SIDSTAR": parse_sidstar(source_dir / config["data"][data_source]["source"]), + "SIDSTAR": parse_sidstar( + source_dir / config["data"][data_source]["source"] + ), + } + elif data_source_type == "sct": + logging.debug(f"Loading SCT source {data_source}...") + self.data[data_source] = { + "RUNWAY": parse_runway( + source_dir / config["data"][data_source]["source"] + ), } else: logging.error(f"Unknown data source type for data source {data_source}") diff --git a/mapbuilder/data/rwy.py b/mapbuilder/data/rwy.py new file mode 100644 index 0000000..e49462b --- /dev/null +++ b/mapbuilder/data/rwy.py @@ -0,0 +1,90 @@ +import re +from pathlib import Path + +from mapbuilder.utils import geo +from mapbuilder.utils.legacy import parse_es_coords + + +def parse_airport(file_path: Path) -> dict: + """Parses Runway information from the SCT""" + pattern = r"^(\w{4})\s+(\d{3}\.\d{3})\s+(\S+)\s+(\S+)\s+(\w)$" + + ads = {} + in_section = False + + with file_path.open("r", encoding="iso-8859-1") as f: + for raw_line in f: + line = raw_line.strip() + if line.startswith(";"): + continue + + if line == "[AIRPORT]": + in_section = True + continue + + if not in_section: + continue + + # Next section + if line.startswith("["): + break + + match = re.search(pattern, line) + + if match: + ads[match.group(1)] = geo.Fix( + parse_es_coords(match.group(3), match.group(4)) + ) + + return ads + + +def parse_runway(file_path: Path) -> dict: + """Parses Runway information from the SCT""" + pattern = r"^(\d{2}[LRC]?)\s+(\d{2}[LRC]?)\s+(\d{3})\s+(\d{3})\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\w{4})$" + + ads = parse_airport(file_path) + runways = {} + in_section = False + + with file_path.open("r", encoding="iso-8859-1") as f: + for raw_line in f: + line = raw_line.strip() + if line.startswith(";"): + continue + + if line == "[RUNWAY]": + in_section = True + continue + + if not in_section: + continue + + # Next section + if line.startswith("["): + break + + match = re.search(pattern, line) + + if match: + rwy1 = match.group(1) + rwy2 = match.group(2) + rwystr = f"{rwy1}-{rwy2}" + icao = match.group(9) + thr1 = geo.Fix(parse_es_coords(match.group(5), match.group(6))) + thr2 = geo.Fix(parse_es_coords(match.group(7), match.group(8))) + + if icao not in runways: + runways[icao] = {} + + runways[icao][rwystr] = { + "rwy1": rwy1, + "rwy2": rwy2, + "brg1": int(match.group(3)), + "brg2": int(match.group(4)), + "thr1": thr1, + "thr2": thr2, + "center": ads[icao], + } + + return runways diff --git a/mapbuilder/handlers/jinja.py b/mapbuilder/handlers/jinja.py index ca687d7..b16ebe2 100644 --- a/mapbuilder/handlers/jinja.py +++ b/mapbuilder/handlers/jinja.py @@ -7,6 +7,7 @@ from shapely import Geometry, Polygon from mapbuilder.data.aixm2 import AIXMFeature +from mapbuilder.utils.ad import render_cl, render_runways from mapbuilder.utils.ecl import draw_ecl_dashes, draw_loc_tick, draw_marker_ticks from mapbuilder.utils.geo import brg, fix from mapbuilder.utils.sidstar import render_sid @@ -32,6 +33,8 @@ def handle(self, item: Path) -> str: render_sid=render_sid, fix=fix, brg=brg, + render_runways=render_runways, + render_cl=render_cl, ) jinja_env.filters.update( geoms=geoms, @@ -170,7 +173,7 @@ def envelope(geometries): return shapely.envelope(geometries) -def to_poly(geometries, designator: str, color: str, coordpoly=False): +def to_poly(geometries, designator: str, color: str | None = None, coordpoly=False): lines = [f"// {designator}"] if designator else [] _render_polygon(lines, _get_geoms(geometries), color) @@ -181,9 +184,12 @@ def to_poly(geometries, designator: str, color: str, coordpoly=False): return "\n".join(lines) -def _render_polygon(lines, polygons, color): +def _render_polygon(lines, polygons, color=None): for polygon in polygons: - lines.append(f"COLOR:{color}") + if color is not None: + lines.append(f"COLOR:{color}") + else: + lines.append("") for point in polygon.coords: lines.append(f"COORD:{coord2es((point[0], point[1]))}") diff --git a/mapbuilder/utils/ad.py b/mapbuilder/utils/ad.py new file mode 100644 index 0000000..d5ba258 --- /dev/null +++ b/mapbuilder/utils/ad.py @@ -0,0 +1,24 @@ +def render_runways(ad: dict, length: float = 1.5, exclude: list | None = None) -> str: + if exclude is None: + exclude = [] + lines = [] + + for rwy_id, data in ad.items(): + if rwy_id in exclude: + continue + + if data["brg1"] == 0 and data["brg2"] == 0: + continue + + lines.append(str(data["center"].move_to(length / 2, data["brg2"]).line_to(length, data["brg1"]))) + + return "\n".join(lines) + + +def render_cl(ad: dict) -> str: + lines = [] + + for _, data in ad.items(): + lines.append(str(data["thr1"].line_to_fix(data["thr2"]))) + + return "\n".join(lines) diff --git a/mapbuilder/utils/legacy.py b/mapbuilder/utils/legacy.py new file mode 100644 index 0000000..f9c715e --- /dev/null +++ b/mapbuilder/utils/legacy.py @@ -0,0 +1,21 @@ +def parse_es_coords(lat_str, lon_str): + def convert_to_decimal(degree, minute, second, direction): + decimal = degree + minute / 60 + second / 3600 + if direction in ["S", "W"]: + decimal = -decimal + return decimal + + lat_deg = int(lat_str[1:4]) + lat_min = int(lat_str[5:7]) + lat_sec = float(lat_str[8:13]) + lat_dir = lat_str[0] + + lon_deg = int(lon_str[1:4]) + lon_min = int(lon_str[5:7]) + lon_sec = float(lon_str[8:13]) + lon_dir = lon_str[0] + + latitude = convert_to_decimal(lat_deg, lat_min, lat_sec, lat_dir) + longitude = convert_to_decimal(lon_deg, lon_min, lon_sec, lon_dir) + + return latitude, longitude