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
  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
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
 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
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
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
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)
OutputClear

Source code in jetfuelburn/utility/geography.py
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
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')
OutputClear

Source code in jetfuelburn/utility/geography.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
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)