# В аэропорту

Примеры интеграции METAR и TAF на уровне аэропорта — категории полётных условий, высота по плотности, прогнозы на вылет и запасные аэропорты.



Готовые к запуску примеры работы с погодой **на уровне аэропорта**: [METAR](/docs/v31/weather-metar) и [TAF](/docs/v31/weather-taf) для аэропортов вылета, назначения и запасных. Запросы с bbox для маршрута (ветер на высотах, [PIREP](https://www.aviationweather.gov/help/data/#pirep), [AIRMET](https://www.aviationweather.gov/help/data/#airmet)) см. в разделе [На маршруте](/docs/v31/weather-integration-enroute).

| # | Вопрос                                                                                                                      | Используемые данные |
| - | --------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| 1 | Какова текущая [категория полётных условий](/docs/v31/weather#flight-categories) в крупных аэропортах США **прямо сейчас**? | METAR               |
| 2 | Превышает ли высота по плотности высоту ВПП больше чем на заданный вами порог?                                              | METAR               |
| 3 | Какой будет погода **в момент моего вылета** (в ближайшие несколько часов)?                                                 | TAF                 |
| 4 | **Нью-Йорк → Лос-Анджелес** — погода при взлёте и посадке: сейчас и по прогнозу?                                            | METAR + TAF         |
| 5 | Сводка текущих METAR в выбранных международных аэропортах                                                                   | METAR               |
| 6 | Проходит ли текущий METAR пользовательский чек-лист по ветру, видимости и нижней границе облаков?                           | METAR               |

<DocRequirements>
  <DocRequirementAuth />
</DocRequirements>

### Установка [#install]

Скрипты ниже используют `requests` для вызовов API, `rich` для таблиц в терминале и `plotly` + `pandas` для карты категорий полётных условий. Установите один раз:

```bash
pip install requests rich plotly pandas
```

***

### 1. Карта текущих категорий полётных условий (METAR в реальном времени) [#1-current-flight-category-map-live-metar]

**Вопрос:** какова текущая [категория полётных условий](/docs/v31/weather#flight-categories) по данным [METAR](https://en.wikipedia.org/wiki/METAR) в четырёх крупных аэропортах США?

[METAR](https://en.wikipedia.org/wiki/METAR) возвращает категорию полётных условий (`VFR` / `MVFR` / `IFR` / `LIFR`). Этот скрипт отмечает каждый аэропорт на карте США цветными подписями согласно таблице [категорий полётных условий](/docs/v31/weather#flight-categories).

```python
import os
import webbrowser
from pathlib import Path

import pandas as pd
import plotly.express as px
import requests

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

# Demo airports: code → (latitude, longitude, city name)
AIRPORTS = {
    "KJFK": (40.6413, -73.7781, "New York (JFK)"),
    "KLAX": (33.9425, -118.4081, "Los Angeles"),
    "KORD": (41.9742, -87.9073, "Chicago"),
    "KDEN": (39.8561, -104.6737, "Denver"),
}

# Flight category → color + label (see METAR reference)
CONDITIONS = {
    "VFR":  ("#22c55e", "VFR - visual flight rules"),
    "MVFR": ("#3b82f6", "MVFR - marginal VFR"),
    "IFR":  ("#ef4444", "IFR - instrument flight rules"),
    "LIFR": ("#a855f7", "LIFR - low IFR"),
}


def fetch_metar(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def build_map() -> Path:
    rows = []
    for icao, (lat, lon, city) in AIRPORTS.items():
        metar = fetch_metar(icao)
        if not metar:
            continue
        p = metar["parsed"]
        code = p["flight_rules"]
        _color, label = CONDITIONS.get(code, ("#6b7280", code))
        wind = p["wind"]["speed"]
        rows.append({
            "city": city,
            "code": icao,
            "lat": lat,
            "lon": lon,
            "rating_code": code,
            "conditions": label,
            "wind": f"{wind} kt (~{round(wind * 1.15)} mph, ~{round(wind * 1.852)} km/h)",
            "map_label": f"{city}: {label}",
        })

    df = pd.DataFrame(rows)
    color_map = {k: v[0] for k, v in CONDITIONS.items()}

    fig = px.scatter_geo(
        df,
        lat="lat",
        lon="lon",
        color="rating_code",
        color_discrete_map=color_map,
        hover_name="city",
        hover_data={
            "conditions": True,
            "wind": True,
            "code": True,
            "rating_code": False,
            "lat": False,
            "lon": False,
            "map_label": False,
        },
        scope="usa",
        title="Current flight category by airport (METAR)",
        labels={"rating_code": "API code"},
    )
    fig.update_traces(
        marker={"size": 18, "line": {"width": 2, "color": "white"}},
        text=df["map_label"],
        textposition="top center",
    )
    fig.add_annotation(
        text="Green = VFR &nbsp;|&nbsp; Blue = MVFR &nbsp;|&nbsp; Red/magenta = IFR/LIFR",
        xref="paper", yref="paper", x=0.5, y=-0.08, showarrow=False,
        font={"size": 12},
    )

    out = Path("weather_map.html")
    fig.write_html(out, include_plotlyjs="cdn")
    return out


if __name__ == "__main__":
    path = build_map()
    print(f"Saved {path} - opening interactive map in your browser.")
    print("Hover a marker for wind and category. Colors follow METAR flight_rules.")
    webbrowser.open(path.resolve().as_uri())
```

**Результат:** HTML-карта с четырьмя аэропортами США, окрашенными по `flight_rules`, со всплывающими данными о ветре и подписью категории.

***

### 2. Высота по плотности и высота ВПП [#2-density-altitude-vs-runway-elevation]

**Вопрос:** превышает ли `parsed.density_altitude` в этом аэропорту высоту ВПП больше чем на заданный вами порог?

[METAR](https://en.wikipedia.org/wiki/METAR) содержит **высоту по плотности** (насколько разрежённым ощущается воздух). Сравните её с высотой ВПП: большая разница означает более длинный разбег при взлёте.

```python
import os

import requests
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"


def fetch_metar(icao: str) -> dict:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    r.raise_for_status()
    return r.json()


def takeoff_performance_report(
    icao: str,
    runway_elevation_ft: int,
    concern_if_da_above_runway_ft: int = 1000,
) -> None:
    metar = fetch_metar(icao)
    name = metar.get("airport_name", icao)
    p = metar["parsed"]
    da = p["density_altitude"]
    gap = da - runway_elevation_ft
    hot_day = gap >= concern_if_da_above_runway_ft

    table = Table(title=f"Takeoff air density - {name} ({icao})")
    table.add_column("What", style="cyan")
    table.add_column("Value", justify="right")
    table.add_row("Runway elevation", f"{runway_elevation_ft} ft above sea level")
    table.add_row("Air feels like elevation", f"{da} ft (density altitude)")
    table.add_row("Difference", f"+{gap} ft - air is thinner than runway height suggests")
    table.add_row("Outside temperature", f"{p['temperature']} C / {round(p['temperature'] * 9/5 + 32)} F")
    table.add_row("Barometer", f"{p['altimeter']} inHg")
    console.print(table)

    if hot_day:
        msg = (
            f"The air is significantly thinner than normal for this runway ({gap} ft gap). "
            "Planes need more runway to lift off and climb slower. "
            "Check your aircraft handbook before assuming normal takeoff distance."
        )
        console.print(Panel(msg, title="WARN: possible reduced takeoff performance", style="red"))
    else:
        msg = (
            f"Density altitude is within {concern_if_da_above_runway_ft} ft of runway elevation. "
            "Takeoff performance is likely close to normal (still verify for your aircraft)."
        )
        console.print(Panel(msg, title="OK: no major thin-air warning for this threshold", style="green"))


if __name__ == "__main__":
    # JFK runway is almost at sea level (~13 ft)
    takeoff_performance_report("KJFK", runway_elevation_ft=13)
```

**Результат:** таблица со сравнением высоты ВПП, высоты по плотности и температуры; панель предупреждает, если разница превышает порог.

***

### 3. Какой будет погода в момент моего вылета? [#3-what-will-the-weather-be-when-i-depart]

**Вопрос:** я планирую взлететь из Чикаго **в текущее время UTC** — каких условий ожидать?

[TAF](https://en.wikipedia.org/wiki/Terminal_aerodrome_forecast) — это временная шкала прогнозных блоков. **Наиболее вероятные условия** = основной блок `FROM`; **возможные кратковременные периоды** = перекрывающиеся строки `TEMPO` / `BECMG`.

```python
import os
from datetime import datetime, timezone

import requests
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

CONDITIONS = {
    "VFR":  "VFR - visual flight rules",
    "MVFR": "MVFR - marginal VFR",
    "IFR":  "IFR - instrument flight rules",
    "LIFR": "LIFR - low IFR",
}

LAYER_MEANING = {
    "FROM":  "Most likely for this time window",
    "TEMPO": "Possible brief worse spell (may not happen)",
    "BECMG": "Conditions gradually changing toward",
}


def parse_time(iso: str) -> datetime:
    return datetime.fromisoformat(iso.replace("Z", "+00:00"))


def fetch_taf(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/taf/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def print_departure_forecast(icao: str, departure: datetime) -> None:
    taf = fetch_taf(icao)
    if not taf:
        console.print(f"[red]No forecast (TAF) published for {icao} - common at small airfields.[/]")
        return

    name = taf.get("airport_name", icao)
    console.print(Panel(
        f"Forecast airport: {name} ({icao})\n"
        f"Departure time (UTC): {departure.strftime('%Y-%m-%d %H:%M')} Z\n"
        f"Below = parsed TAF layers active at that instant.",
        title="Question: forecast at departure time",
    ))

    forecast = taf["parsed"]["forecast"]
    baseline = next(
        (
            p for p in forecast
            if p["type"] == "FROM"
            and parse_time(p["start_time"]) <= departure <= parse_time(p["end_time"])
        ),
        None,
    )
    modifiers = [
        p for p in forecast
        if p is not baseline
        and parse_time(p["start_time"]) <= departure <= parse_time(p["end_time"])
    ]

    table = Table(title="Forecast layers at your departure time")
    table.add_column("Role", style="cyan")
    table.add_column("Conditions")
    table.add_column("Weather details")

    if baseline:
        code = baseline["flight_rules"]
        wx = ", ".join(w["repr"] for w in baseline["wx_codes"]) or "No significant weather"
        table.add_row(
            LAYER_MEANING.get("FROM", "Most likely"),
            CONDITIONS.get(code, code),
            wx,
        )
    else:
        table.add_row("Most likely", "-", "No matching forecast block for this time")

    for p in modifiers:
        code = p["flight_rules"]
        wx = ", ".join(w["repr"] for w in p["wx_codes"]) or "-"
        prob = (p.get("probability") or {}).get("value")
        role = LAYER_MEANING.get(p["type"], p["type"])
        if prob:
            role += f" ({prob}% chance)"
        table.add_row(role, CONDITIONS.get(code, code), wx)

    console.print(table)
    console.print(
        "\n[dim]Show the baseline FROM row and any overlapping TEMPO/BECMG rows separately.[/dim]"
    )


if __name__ == "__main__":
    print_departure_forecast("KORD", datetime.now(timezone.utc))
```

**Результат:** панель со временем вылета и таблица с действующей строкой `FROM` и перекрывающимися периодами-модификаторами.

***

### 4. Нью-Йорк → Лос-Анджелес — погода в обоих аэропортах? [#4-new-york--los-angeles---weather-at-both-ends]

**Вопрос:** я лечу по маршруту **KJFK → KLAX** — какая погода в аэропортах вылета и назначения **прямо сейчас** и каков **наихудший прогноз** на ближайшие \~24 часа?

```python
import os
import concurrent.futures

import requests
from rich.console import Console
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

CONDITIONS = {
    "VFR":  "VFR - visual flight rules",
    "MVFR": "MVFR - marginal VFR",
    "IFR":  "IFR - instrument flight rules",
    "LIFR": "LIFR - low IFR",
}
SEVERITY = {"VFR": 0, "MVFR": 1, "IFR": 2, "LIFR": 3}
STYLE = {"VFR": "green", "MVFR": "blue", "IFR": "red", "LIFR": "magenta"}


def fetch_metar(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def fetch_taf(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/taf/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def worst_forecast_label(taf: dict | None) -> str:
    if not taf:
        return "No forecast available"
    from_blocks = [p for p in taf["parsed"]["forecast"] if p["type"] == "FROM"]
    if not from_blocks:
        return "No forecast available"
    worst = max(from_blocks, key=lambda p: SEVERITY.get(p["flight_rules"], -1))
    code = worst["flight_rules"]
    return CONDITIONS.get(code, code)


def print_route_weather(dep_icao: str, arr_icao: str) -> None:
    with concurrent.futures.ThreadPoolExecutor() as ex:
        metar_dep = ex.submit(fetch_metar, dep_icao).result()
        taf_dep = ex.submit(fetch_taf, dep_icao).result()
        metar_arr = ex.submit(fetch_metar, arr_icao).result()
        taf_arr = ex.submit(fetch_taf, arr_icao).result()

    table = Table(title=f"Route weather: {dep_icao} (departure) -> {arr_icao} (arrival)")
    table.add_column("Airport", style="cyan")
    table.add_column("Weather RIGHT NOW", max_width=36)
    table.add_column("Worst forecast in next ~24 h", max_width=36)

    for icao, metar, taf in [
        (dep_icao, metar_dep, taf_dep),
        (arr_icao, metar_arr, taf_arr),
    ]:
        if metar:
            code = metar["parsed"]["flight_rules"]
            now = CONDITIONS.get(code, code)
            style = STYLE.get(code, "white")
            airport = metar.get("airport_name", icao)
        else:
            now, style, airport = "No observation", "white", icao

        table.add_row(
            f"{airport}\n({icao})",
            f"[{style}]{now}[/]",
            worst_forecast_label(taf),
        )

    console.print(table)
    console.print(
        "\n[dim]RIGHT NOW = live METAR observation. "
        "Worst forecast = poorest 'most likely' TAF block in the next day.[/dim]"
    )


if __name__ == "__main__":
    print_route_weather("KJFK", "KLAX")
```

**Результат:** две строки (вылет и прилёт): текущие `flight_rules` из [METAR](https://en.wikipedia.org/wiki/METAR) и наихудшая категория `FROM` из [TAF](https://en.wikipedia.org/wiki/Terminal_aerodrome_forecast).

***

### 5. Дашборд METAR по нескольким аэропортам [#5-multi-airport-metar-dashboard]

**Вопрос:** получить декодированный [METAR](https://en.wikipedia.org/wiki/METAR) для фиксированного списка аэропортов и вывести ветер, температуру, видимость и `flight_rules`.

```python
import os

import requests
from rich.console import Console
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

AIRPORTS = ["KJFK", "KLAX", "EGLL", "YSSY"]  # New York, LA, London, Sydney

CONDITIONS = {
    "VFR":  "VFR - visual flight rules",
    "MVFR": "MVFR - marginal VFR",
    "IFR":  "IFR - instrument flight rules",
    "LIFR": "LIFR - low IFR",
}
STYLE = {"VFR": "green", "MVFR": "blue", "IFR": "red", "LIFR": "magenta"}


def fetch_metar(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def visibility_text(parsed: dict) -> str:
    v = parsed["visibility"]["value"]
    repr_ = parsed["visibility"]["repr"]
    if v is None:
        return "More than 6 miles visibility"
    # Some international stations return coded values (e.g. 9999 = 10 km+) - not statute miles.
    if v > 50 or repr_ in ("9999", "P6"):
        return f"{repr_} (coded - check raw METAR for units)"
    return f"{v} miles visibility"


def print_live_dashboard(airports: list[str]) -> None:
    table = Table(title="Live airport weather (METAR - updates every 30–60 min)")
    table.add_column("Airport", style="cyan", max_width=28)
    table.add_column("Flight category", max_width=30)
    table.add_column("Wind")
    table.add_column("Temp")
    table.add_column("Visibility")

    for icao in airports:
        data = fetch_metar(icao)
        if not data:
            table.add_row(icao, "No data", "-", "-", "-")
            continue
        p = data["parsed"]
        code = p["flight_rules"]
        label = CONDITIONS.get(code, code)
        style = STYLE.get(code, "white")
        gust = p["wind"]["gust"]
        wind = f"{p['wind']['speed']} kt"
        if gust:
            wind += f" (gusts {gust} kt)"
        table.add_row(
            data["airport_name"],
            f"[{style}]{label}[/]",
            wind,
            f"{p['temperature']} C",
            visibility_text(p),
        )

    console.print(table)
    console.print(
        "\n[dim]In a real dashboard, refresh every 5 minutes - "
        "that is often enough and saves API quota.[/dim]"
    )


if __name__ == "__main__":
    print_live_dashboard(AIRPORTS)
```

**Результат:** таблица в терминале с названием аэропорта, подписью `flight_rules`, ветром, температурой и видимостью.

***

### 6. Чек-лист личных минимумов (демо) [#6-personal-limits-checklist-demo]

**Вопрос:** проходит ли текущий [METAR](https://en.wikipedia.org/wiki/METAR) проверку по настраиваемым ограничениям ветра, порывов, видимости и нижней границы облаков?

Не заменяет официальный метеобрифинг или нормативные минимумы.

```python
import os

import requests
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

# Demo personal limits - change these to match your skills / aircraft
MY_LIMITS = {
    "max_wind_kt": 25,
    "max_gust_kt": 35,
    "min_visibility_sm": 1.0,
    "min_ceiling_ft": 500,
}


def fetch_metar(icao: str) -> dict:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    r.raise_for_status()
    return r.json()


def print_safety_check(icao: str) -> None:
    metar = fetch_metar(icao)
    name = metar.get("airport_name", icao)
    p = metar["parsed"]
    code = p["flight_rules"]

    limits = Table(title=f"Your demo limits (edit MY_LIMITS in the script)")
    limits.add_column("Rule")
    limits.add_column("Your limit", justify="right")
    limits.add_row("Max steady wind", f"{MY_LIMITS['max_wind_kt']} kt")
    limits.add_row("Max gusts", f"{MY_LIMITS['max_gust_kt']} kt")
    limits.add_row("Min visibility", f"{MY_LIMITS['min_visibility_sm']} miles")
    limits.add_row("Min cloud ceiling", f"{MY_LIMITS['min_ceiling_ft']} ft")
    console.print(limits)

    problems: list[str] = []

    wind = p["wind"]["speed"]
    if wind > MY_LIMITS["max_wind_kt"]:
        problems.append(f"Wind too strong: {wind} kt (your max {MY_LIMITS['max_wind_kt']} kt)")

    gust = p["wind"]["gust"]
    if gust and gust > MY_LIMITS["max_gust_kt"]:
        problems.append(f"Gusts too strong: {gust} kt (your max {MY_LIMITS['max_gust_kt']} kt)")

    vis = p["visibility"]["value"]
    if vis is not None and vis < MY_LIMITS["min_visibility_sm"]:
        problems.append(
            f"Visibility too low: {vis} miles (you need at least {MY_LIMITS['min_visibility_sm']} mi)"
        )

    ceiling_layers = [c for c in p["clouds"] if c["type"] in ("BKN", "OVC")]
    if ceiling_layers:
        ceiling_ft = ceiling_layers[0]["base"] * 100
        if ceiling_ft < MY_LIMITS["min_ceiling_ft"]:
            problems.append(
                f"Clouds too low: base at {ceiling_ft} ft (you need at least {MY_LIMITS['min_ceiling_ft']} ft)"
            )

    safe = len(problems) == 0
    if safe:
        body = (
            f"All demo checks passed at {name}.\n"
            f"Current overall rating from METAR: {code}.\n"
            "This does NOT replace a full weather briefing."
        )
        title = f"Within demo limits - {icao}"
        style = "green"
    else:
        body = "Problems found:\n" + "\n".join(f"- {x}" for x in problems)
        title = f"Exceeds demo limits - {icao}"
        style = "red"

    console.print(Panel(body, title=title, style=style))


if __name__ == "__main__":
    print_safety_check("KJFK")
```

**Результат:** таблица ограничений, затем панель со списком пройденных проверок или с каждым нарушенным правилом относительно `MY_LIMITS`.

***

## Погода на маршруте (эндпоинты с bbox) [#en-route-weather-bbox-endpoints]

Вопросы 7–9 запрашивают данные в пределах &#x2A;*ограничивающего прямоугольника (bounding box)** вдоль вашего маршрута. Постройте прямоугольник по координатам аэропортов вылета и назначения (с запасом) или по путевым точкам. Пустой `reports` / `total: 0` — нормальная ситуация, если [PIREP](https://www.aviationweather.gov/help/data/#pirep) не подавались или нет действующих предупреждений, — это не сбой API.

Вспомогательная функция, используемая ниже:

```python
def route_bbox(
    dep_lat: float, dep_lon: float,
    arr_lat: float, arr_lon: float,
    pad_deg: float = 2.0,
) -> str:
    """SW corner first, then NE - matches API bbox format."""
    lat1 = min(dep_lat, arr_lat) - pad_deg
    lat2 = max(dep_lat, arr_lat) + pad_deg
    lon1 = min(dep_lon, arr_lon) - pad_deg
    lon2 = max(dep_lon, arr_lon) + pad_deg
    return f"{lat1},{lon1},{lat2},{lon2}"
```

Координаты **KJFK** (40.6413, -73.7781) и **KLAX** (33.9425, -118.4081).
