Skip to content

Geography

jetfuelburn.utility.geography

_AirportAtlas

Singleton-style class to manage airport data loading and lookups. Lazy-loads the dataset on the first request and indexes it by IATA code, ICAO code, and Name simultaneously to avoid repeated file I/O.

Source code in jetfuelburn/utility/geography.py
class _AirportAtlas:
    r"""
    Singleton-style class to manage airport data loading and lookups.
    Lazy-loads the dataset on the first request and indexes it by
    IATA code, ICAO code, and Name simultaneously to avoid repeated file I/O.
    """

    def __init__(self):
        self._loaded = False
        self._iata_index: dict[str, dict] = {}
        self._icao_index: dict[str, dict] = {}
        self._name_index: dict[str, dict] = {}

    def _load_data(self):
        r"""
        Loads the GZIP containing airport data into memory and builds indices.

        See Also
        --------
        [`ip2location-iata-icao`](https://github.com/ip2location/ip2location-iata-icao) list of airport codes, names and coordinates on GitHub.
        """
        if self._loaded:
            return

        path = files("jetfuelburn.data.Airports").joinpath("airports.csv.gz")

        with path.open("rb") as binary_file:
            with gzip.open(binary_file, mode="rt", encoding="utf-8") as text_file:
                reader = csv.DictReader(text_file)

                for row in reader:
                    try:
                        row["latitude"] = float(row["latitude"])
                        row["longitude"] = float(row["longitude"])
                    except (ValueError, KeyError):
                        continue  # Skip invalid rows

                    if "airport" in row:
                        row["name"] = row.pop("airport")

                    if iata := row.get("iata"):
                        self._iata_index[iata] = row

                    if icao := row.get("icao"):
                        self._icao_index[icao] = row

                    if name := row.get("name"):
                        self._name_index[name] = row

        self._loaded = True

    def _get_airport(self, identifier: str, by: str = "iata") -> dict:
        r"""
        Retrieves an airport data dictionary by the specified identifier.

        Parameters
        ----------
        identifier : str
            The airport identifier (IATA code, ICAO code, or Name).
        by : str, optional
            The type of identifier provided. Must be one of:
            `icao`: International Civil Aviation Organization code
            `iata`: International Air Transport Association code
            `name`: Full airport name
            Default is `iata`.

        Returns
        -------
        dict or None
            A dictionary containing airport data if found, otherwise `None`.
            For instance, for `by='name'` and `identifier='Al Ain International Airport'`:
            ```python
                {
                    'iata': 'AAN',
                    'icao': 'OMAL',
                    'name': 'Al Ain International Airport',
                    'latitude': 24.2617,
                    'longitude': 55.6092
                }
            ```
            For instance, for `by='icao'` and `identifier='LOWI'`:
            ```python
            {
                'iata': 'INN',
                'icao': 'LOWI',
                'airport': 'Innsbruck Airport (Kranebitten Airport)',
                'latitude': 47.2602,
                'longitude': 11.344
            }
            ```

        Raises
        ------
        ValueError
            If `by` is not one of 'iata', 'icao', or 'name'.
        """
        if not self._loaded:
            self._load_data()

        if by == "iata":
            return self._iata_index.get(identifier)
        elif by == "icao":
            return self._icao_index.get(identifier)
        elif by == "name":
            return self._name_index.get(identifier)
        else:
            raise ValueError(
                f"Invalid identifier type: '{by}'. Must be 'iata', 'icao', or 'name'."
            )

_get_airport

_get_airport(identifier, by='iata')

Retrieves an airport data dictionary by the specified identifier.

Parameters:

Name Type Description Default
identifier str

The airport identifier (IATA code, ICAO code, or Name).

required
by str

The type of identifier provided. Must be one of: icao: International Civil Aviation Organization code iata: International Air Transport Association code name: Full airport name Default is iata.

'iata'

Returns:

Type Description
dict or None

A dictionary containing airport data if found, otherwise None. For instance, for by='name' and identifier='Al Ain International Airport':

    {
        'iata': 'AAN',
        'icao': 'OMAL',
        'name': 'Al Ain International Airport',
        'latitude': 24.2617,
        'longitude': 55.6092
    }
For instance, for by='icao' and identifier='LOWI':
{
    'iata': 'INN',
    'icao': 'LOWI',
    'airport': 'Innsbruck Airport (Kranebitten Airport)',
    'latitude': 47.2602,
    'longitude': 11.344
}

Raises:

Type Description
ValueError

If by is not one of 'iata', 'icao', or 'name'.

Source code in jetfuelburn/utility/geography.py
def _get_airport(self, identifier: str, by: str = "iata") -> dict:
    r"""
    Retrieves an airport data dictionary by the specified identifier.

    Parameters
    ----------
    identifier : str
        The airport identifier (IATA code, ICAO code, or Name).
    by : str, optional
        The type of identifier provided. Must be one of:
        `icao`: International Civil Aviation Organization code
        `iata`: International Air Transport Association code
        `name`: Full airport name
        Default is `iata`.

    Returns
    -------
    dict or None
        A dictionary containing airport data if found, otherwise `None`.
        For instance, for `by='name'` and `identifier='Al Ain International Airport'`:
        ```python
            {
                'iata': 'AAN',
                'icao': 'OMAL',
                'name': 'Al Ain International Airport',
                'latitude': 24.2617,
                'longitude': 55.6092
            }
        ```
        For instance, for `by='icao'` and `identifier='LOWI'`:
        ```python
        {
            'iata': 'INN',
            'icao': 'LOWI',
            'airport': 'Innsbruck Airport (Kranebitten Airport)',
            'latitude': 47.2602,
            'longitude': 11.344
        }
        ```

    Raises
    ------
    ValueError
        If `by` is not one of 'iata', 'icao', or 'name'.
    """
    if not self._loaded:
        self._load_data()

    if by == "iata":
        return self._iata_index.get(identifier)
    elif by == "icao":
        return self._icao_index.get(identifier)
    elif by == "name":
        return self._name_index.get(identifier)
    else:
        raise ValueError(
            f"Invalid identifier type: '{by}'. Must be 'iata', 'icao', or 'name'."
        )

_load_data

_load_data()

Loads the GZIP containing airport data into memory and builds indices.

See Also

ip2location-iata-icao list of airport codes, names and coordinates on GitHub.

Source code in jetfuelburn/utility/geography.py
def _load_data(self):
    r"""
    Loads the GZIP containing airport data into memory and builds indices.

    See Also
    --------
    [`ip2location-iata-icao`](https://github.com/ip2location/ip2location-iata-icao) list of airport codes, names and coordinates on GitHub.
    """
    if self._loaded:
        return

    path = files("jetfuelburn.data.Airports").joinpath("airports.csv.gz")

    with path.open("rb") as binary_file:
        with gzip.open(binary_file, mode="rt", encoding="utf-8") as text_file:
            reader = csv.DictReader(text_file)

            for row in reader:
                try:
                    row["latitude"] = float(row["latitude"])
                    row["longitude"] = float(row["longitude"])
                except (ValueError, KeyError):
                    continue  # Skip invalid rows

                if "airport" in row:
                    row["name"] = row.pop("airport")

                if iata := row.get("iata"):
                    self._iata_index[iata] = row

                if icao := row.get("icao"):
                    self._icao_index[icao] = row

                if name := row.get("name"):
                    self._name_index[name] = row

    self._loaded = True

_calculate_haversine_distance

_calculate_haversine_distance(lat1, lon1, lat2, lon2)

Calculates the Great Circle distance between two points on the earth (specified in decimal degrees).

\[\begin{align*} a &= \sin^2\left(\frac{\Delta\phi}{2}\right) + \cos \phi_1 \cdot \cos \phi_2 \cdot \sin^2\left(\frac{\Delta\lambda}{2}\right) \\ c &= 2 \cdot \operatorname{atan2}\left(\sqrt{a}, \sqrt{1-a}\right) \\ d &= R \cdot c \end{align*}\]

where:

Symbol Description
\(\phi\), \(\lambda\) Latitude and Longitude
\(\Delta\phi\), \(\Delta\lambda\) Difference in latitude and longitude (in radians)
\(R\) Mean earth radius (approx. 6,371 km)
See Also

Haversine formula entry on Wikipedia
Great-circle distance entry on Wikipedia
Earth radius entry on Wikipedia

Parameters:

Name Type Description Default
lat1 float

Latitude of point 1 in decimal degrees.

required
lon1 float

Longitude of point 1 in decimal degrees.

required
lat2 float

Latitude of point 2 in decimal degrees.

required
lon2 float

Longitude of point 2 in decimal degrees.

required

Returns:

Type Description
Quantity

The distance between the two points [length].

Example

Editor (session: default) Run
import jetfuelburn
from jetfuelburn.utility.geography import _calculate_haversine_distance
_calculate_haversine_distance(52.3086, 4.7639, 51.4700, -0.4543)
Output Clear
Source code in jetfuelburn/utility/geography.py
def _calculate_haversine_distance(lat1, lon1, lat2, lon2):
    r"""
    Calculates the Great Circle distance between two points 
    on the earth (specified in decimal degrees).

    \begin{align*}
    a &= \sin^2\left(\frac{\Delta\phi}{2}\right) + \cos \phi_1 \cdot \cos \phi_2 \cdot \sin^2\left(\frac{\Delta\lambda}{2}\right) \\
    c &= 2 \cdot \operatorname{atan2}\left(\sqrt{a}, \sqrt{1-a}\right) \\
    d &= R \cdot c
    \end{align*}

    where:

    | Symbol                        | Description                                      |
    |-------------------------------|--------------------------------------------------|
    | $\phi$, $\lambda$             | Latitude and Longitude                           |
    | $\Delta\phi$, $\Delta\lambda$ | Difference in latitude and longitude (in radians)|
    | $R$                           | Mean earth radius (approx. 6,371 km)             |

    See Also
    --------
    [Haversine formula entry on Wikipedia](https://en.wikipedia.org/wiki/Haversine_formula)  
    [Great-circle distance entry on Wikipedia](https://en.wikipedia.org/wiki/Great-circle_distance)  
    [Earth radius entry on Wikipedia](https://en.wikipedia.org/wiki/Earth_radius)

    Parameters
    ----------
    lat1 : float
        Latitude of point 1 in decimal degrees.
    lon1 : float
        Longitude of point 1 in decimal degrees.
    lat2 : float
        Latitude of point 2 in decimal degrees.
    lon2 : float
        Longitude of point 2 in decimal degrees.

    Returns
    -------
    Quantity
        The distance between the two points [length].

    Example
    -------
    ```pyodide install='jetfuelburn'
    import jetfuelburn
    from jetfuelburn.utility.geography import _calculate_haversine_distance
    _calculate_haversine_distance(52.3086, 4.7639, 51.4700, -0.4543)
    ```
    """
    if not (-90 <= lat1 <= 90 and -90 <= lat2 <= 90):
        raise ValueError("Latitude must be between -90 and 90 degrees.")
    if not (-180 <= lon1 <= 180 and -180 <= lon2 <= 180):
        raise ValueError("Longitude must be between -180 and 180 degrees.")

    R = 6371.0 * ureg.km  # Earth radius

    #  Decimal degrees to radians
    d_lat = math.radians(lat2 - lat1)
    d_lon = math.radians(lon2 - lon1)

    a = (math.sin(d_lat / 2) ** 2) + math.cos(math.radians(lat1)) * math.cos(
        math.radians(lat2)
    ) * (math.sin(d_lon / 2) ** 2)

    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))

    return R * c

calculate_distance_between_airports

calculate_distance_between_airports(
    origin, destination, identifier="iata"
)

Calculates the Great Circle distance between two airports. The list of airports is sourced from the ip2location-iata-icao dataset.

Great Circle route between Sydney (SYD) and London Heathrow (LHR). On a mercator projection, the shortest path appears curved.
See Also

ip2location-iata-icao list of airport codes, names and coordinates on GitHub.

Parameters:

Name Type Description Default
origin str

The origin airport code or name.

required
destination str

The destination airport code or name.

required
identifier str

The type of airport identifier provided. Must be one of:

icao: International Civil Aviation Organization code iata: International Air Transport Association code name: Full airport name

Default is iata.

'iata'

Returns:

Type Description
Quantity

The distance between the two airports as a pint.Quantity in kilometers.

Raises:

Type Description
ValueError

If either airport code/name is not found.

Example

Editor (session: default) Run
import jetfuelburn
from jetfuelburn.utility.geography import calculate_distance_between_airports
calculate_distance_between_airports('LOWI', 'LOWW', 'icao')
Output Clear
Source code in jetfuelburn/utility/geography.py
def calculate_distance_between_airports(
    origin: str, destination: str, identifier: str = "iata"
):
    r"""
    Calculates the Great Circle distance between two airports.
    The list of airports is sourced from the `ip2location-iata-icao` dataset.

    ```python exec="true" html="true"
    import plotly.graph_objects as go

    # SYD: -33.9399, 151.1753
    # LHR: 51.4700, -0.4543
    lats = [-33.9399, 51.4700]
    lons = [151.1753, -0.4543]

    fig = go.Figure(data=go.Scattergeo(
        lat=lats,
        lon=lons,
        mode='lines+markers',
        line=dict(width=2, color='blue'),
        marker=dict(size=8, color='red'),
        text=['SYD', 'LHR'],
    ))

    fig.update_layout(
        height=500,
        margin={"r":0,"t":30,"l":0,"b":0},
        geo=dict(
            projection_type="natural earth",
            showland=True,
            landcolor="rgb(243, 243, 243)",
            countrycolor="rgb(204, 204, 204)",
        )
    )

    print(fig.to_html(full_html=False, include_plotlyjs="cdn"))
    ```
    _Great Circle route between Sydney (SYD) and London Heathrow (LHR).
    On a mercator projection, the shortest path appears curved._

    See Also
    --------
    [`ip2location-iata-icao`](https://github.com/ip2location/ip2location-iata-icao) list of airport codes, names and coordinates on GitHub.

    Parameters
    ----------
    origin : str
        The origin airport code or name.
    destination : str
        The destination airport code or name.
    identifier : str, optional
        The type of airport identifier provided. Must be one of:

        `icao`: International Civil Aviation Organization code
        `iata`: International Air Transport Association code
        `name`: Full airport name

        Default is `iata`.
    Returns
    -------
    Quantity
        The distance between the two airports as a `pint.Quantity` in kilometers.

    Raises
    ------
    ValueError
        If either airport code/name is not found.

    Example
    -------
    ```pyodide install='jetfuelburn'
    import jetfuelburn
    from jetfuelburn.utility.geography import calculate_distance_between_airports
    calculate_distance_between_airports('LOWI', 'LOWW', 'icao')
    ```
    """
    origin_info = _atlas._get_airport(origin, by=identifier)
    destination_info = _atlas._get_airport(destination, by=identifier)

    if not origin_info:
        raise ValueError(
            f"Origin airport '{origin}' not found using identifier '{identifier}'."
        )
    if not destination_info:
        raise ValueError(
            f"Destination airport '{destination}' not found using identifier '{identifier}'."
        )

    lat1 = origin_info["latitude"]
    lon1 = origin_info["longitude"]
    lat2 = destination_info["latitude"]
    lon2 = destination_info["longitude"]

    return _calculate_haversine_distance(lat1, lon1, lat2, lon2)