How to access airspace information?

Each Air Control Center (ACC), in charge of providing air traffic control services to controlled flights within its airspace, is subdivided into elementary sectors that are used or combined to build control sectors operated by a pair of air traffic controllers.

Airspace sectorisation consists in partitioning the overall ACC airspace into a given number of these control sectors. In most centres, the set of control sectors deployed, i.e. the sector configuration, varies throughout the day. Basically, sectors are split when controllers’ workload increases, and merged when it decreases.

An Airspace is represented as a set of polygons of geographical coordinates, extruded to a lower and upper altitudes. Airspaces can be of different types, corresponding to different kinds of activities allowed (civil, military, glider, etc.) or services provided.

  • a Flight Information Region (FIR) is the largest regular division of airspace in use in the world today, roughly corresponding to a country, although some FIRs encompass several countries, and larger countries are subdivided into a number of regional FIRs.
    Read more about FIRs of the world
  • a Terminal Maneuvering Area (TMA) is usually controlled by the airport it is attached to: this airspace is usually reserved for aircraft landing at or taking off from that airport;
  • an Air Route Traffic Control Center (ARTCC) or an Air Control Center (ACC) represents the area that is controlled by people located in the same center;
  • an elementary sector is usually the smallest subdivision of airspace that can be controlled by a pair of air traffic controllers.

FIRs from countries in the EUROCONTROL area

Airspaces are usually handled as a wrapper around GeoDataFrame, i.e. Pandas data frames with a particular geometry feature.

from traffic.data import eurofirs

eurofirs.head()
geometry designator name type upper lower latitude longitude
0 POLYGON ((6.19247 49.96404, 6.19405 49.96043, ... EBBU BRUSSELS FIR FIR 195.0 0.0 50.628406 4.600560
1 POLYGON ((6.90560 45.67116, 6.90687 45.67508, ... LIMM MILANO FIR FIR 195.0 0.0 45.030624 10.499386
2 POLYGON ((6.12122 46.25472, 6.12455 46.25131, ... LFMM MARSEILLE FIR FIR 195.0 0.0 42.845677 5.925558
3 POLYGON ((-13.00000 45.00000, -13.00000 43.000... LPPO SANTA MARIA OCEANIC FIR FIR inf 0.0 34.190600 -29.363399
4 POLYGON ((30.56583 51.30833, 30.56722 51.31389... UKBV KYIV FIR FIR 275.0 0.0 50.086590 30.888969

As FIRs usually consist of a single polygon with an upper and a lower limit (in Flight Level, i.e. FL 300 corresponds to 30,000 ft), their representation is simple in a Jupyter environment.

eurofirs["ENOB"]
BODO OCEANIC FIR [ENOB] (FIR)
  • lower: 0 | upper: inf

Leaflet representations are also easily accessible:

eurofirs["EKDK"].map_leaflet()

The following snippet plots the whole dataset:

import matplotlib.pyplot as plt

from cartes.crs import LambertConformal
from cartes.utils.features import countries, lakes, ocean

fig, ax = plt.subplots(
    figsize=(15, 10),
    subplot_kw=dict(projection=LambertConformal(10, 45)),
)

ax.set_extent((-20, 45, 25, 75))

ax.add_feature(countries(scale="50m"))
ax.add_feature(lakes(scale="50m"))
ax.add_feature(ocean(scale="50m"))

ax.spines['geo'].set_visible(False)

for fir in eurofirs:
    fir.plot(ax, edgecolor="#3a3aaa")
    if fir.designator not in ["ENOB", "LPPO", "GCCC"] and fir.area > 1e11:
        fir.annotate(
            ax,
            s=fir.designator,
            ha="center",
            color="#3a3aaa",
            fontname="Ubuntu",
            fontsize=13,
        )
../_images/airspaces_3_0.png

FIRs and ARTCC from the FAA

The Federal Aviation Administration (FAA) publishes some data about their airspace in open data. The data is automatically downloaded the first time you try to access it.

Find more about this service here. On the following map, Air Route Traffic Control Centers (ARTCC) are displayed together with neighbouring FIRs.

from traffic.data.faa import airspace_boundary
import matplotlib.pyplot as plt

from cartes.crs import AzimuthalEquidistant
from cartes.utils.features import countries

fig, ax = plt.subplots(
    figsize=(10, 10),
    subplot_kw=dict(projection=AzimuthalEquidistant(central_longitude=-100)),
)

ax.add_feature(countries(scale="50m"))
ax.set_extent((-130, -65, 15, 60))
ax.spines['geo'].set_visible(False)


for airspace in airspace_boundary.query(
    'type == "FIR" and designator.str[0] in ["C", "M", "T"]'
):
    airspace.plot(ax, edgecolor="#f58518", lw=3, alpha=0.5)

for airspace in airspace_boundary.query(
    'type == "ARTCC" and designator != "ZAN"'  # Anchorage
).at_level(100):
    airspace.plot(ax, edgecolor="#4c78a8", lw=2)
    airspace.annotate(
        ax,
        s=airspace.designator,
        color="#4c78a8",
        ha="center",
        fontname="Ubuntu",
        fontsize=14,
    )
../_images/airspaces_5_0.png

Airspace data from EUROCONTROL

Warning

Access conditions and configuration for these sources of data is detailed here.

Tip

According to the source of data you may have access to, different parsers are implemented but they expose the same API. Data is usually not exactly consistent (coordinates, types, etc.) but you should still be able to safely replace nm_airspaces with aixm_airspaces in the following examples.

from traffic.data import nm_airspaces

Get a single airspace

In these files, airspaces being a composition of elementary airspaces are widespread. Their union is computed and yields a list of polygons associated with minimum and maximum flight levels.

nm_airspaces["EDYYUTAX"]
MUAC EXTENDED [EDYYUTAX] (CRAS)
  • lower: 245 | upper: 255
  • lower: 255 | upper: 295
  • lower: 295 | upper: inf

The Leaflet view and the Matplotlib view, flatten the polygon prior to displaying it:

nm_airspaces["EDYYUTAX"].map_leaflet()

Get many airspaces

Airspaces are stored as a GeoDataFrame, so all pandas operators may be applied to get a subset of them, for example based on their types or designator.

All available airspace types can be accessed here:

nm_airspaces.data["type"].unique()
array(['AREA', 'FIR', 'NAS', 'AUAG', 'CS', 'ES', 'AUA', 'REG', 'CRSA',
       'ERSA', 'CRAS', 'ERAS', 'CLUS'], dtype=object)

In the following examples, we get all FIR spaces in the LF domain (France). You may notice that a single designator may be represented by several entries in the GeoDataFrame, but the representation of each shape you get through iteration, indexation or __geo_interface__ attribute (used in Altair) is properly computed.

france_fir = nm_airspaces.query('type == "FIR" and designator.str.startswith("LF")')
france_fir.head(10)
designator component name type geometry upper lower
218607 LFBBFIR LFBBFIR NaN FIR POLYGON ((2.83333 46.75000, 2.83778 46.72917, ... 195.0 0.0
218615 LFEEFIR LFEEFIR NaN FIR POLYGON ((6.00945 46.54361, 6.13250 46.58305, ... 195.0 0.0
218616 LFEEFIR LFEEFIR NaN FIR POLYGON ((6.71667 47.05333, 6.64167 47.02445, ... 195.0 0.0
218617 LFEEFIR LFEEFIR NaN FIR POLYGON ((5.99917 46.99861, 6.02639 46.98417, ... 195.0 0.0
218618 LFEEFIR LFEEFIR NaN FIR POLYGON ((5.18333 46.70000, 5.33333 46.70000, ... 195.0 0.0
218619 LFEEFIR LFEEFIR NaN FIR POLYGON ((6.39667 46.78222, 6.39167 46.74333, ... 195.0 0.0
218620 LFEEFIR LFEEFIR NaN FIR POLYGON ((6.39667 46.78222, 6.44139 46.76000, ... 195.0 0.0
218621 LFEEFIR LFEEFIR NaN FIR POLYGON ((6.00000 49.45555, 6.05556 49.46667, ... 195.0 0.0
218626 LFFFFIR LFFFFIR NaN FIR POLYGON ((2.00000 51.11667, 2.24500 51.10139, ... 195.0 0.0
218627 LFFFFIR LFFFFIR NaN FIR POLYGON ((2.81667 49.15000, 6.00000 49.45555, ... 195.0 0.0
import altair as alt

from cartes.atlas import europe

france = alt.Chart(europe.topo_feature).transform_filter(
    "datum.properties.geounit == 'France'"
)

base = (
    alt.Chart(france_fir)
    .mark_geoshape(stroke="white")
    # In this dataset, UIR and FIR are both tagged as FIR
    # => we reconstruct the type and designator
    .transform_calculate(suffix="slice(datum.designator, -3)")
    .transform_calculate(designator="slice(datum.designator, 0, -3)")
    .properties(width=300)
)

chart = (
    alt.concat(
        alt.layer(
            base.encode(alt.Color("designator:N", title="FIR"))
            .transform_filter("datum.suffix == 'FIR'"),
            france.mark_geoshape(stroke="#ffffff", strokeWidth=3, fill="#ffffff00"),
            france.mark_geoshape(stroke="#79706e", strokeWidth=1, fill="#ffffff00"),
            base.mark_text(fontSize=14, font="Lato")
            .encode(
                alt.Latitude("latitude:Q"),
                alt.Longitude("longitude:Q"),
                alt.Text("designator:N"),
            )
            .transform_filter("datum.suffix == 'FIR'"),
        ),
        alt.layer(
            base.encode(alt.Color("designator:N"))
            .transform_filter("datum.suffix == 'UIR'"),
            france.mark_geoshape(stroke="#ffffff", strokeWidth=3, fill="#ffffff00"),
            france.mark_geoshape(stroke="#79706e", strokeWidth=1, fill="#ffffff00"),
        )
    )
    .configure_legend(orient="bottom", labelFontSize=12, titleFontSize=12)
    .configure_view(stroke=None)
)

chart

Warning

Note that the FIR and UIR boundaries do not presume anything about how air traffic control is organised in that area. See below a map of the areas controlled by different en-route air traffic control centres in France.

centres = ["LFBBBDX", "LFRRBREST", "LFEERMS", "LFFFPARIS", "LFMMRAW", "LFMMRAE"]
subset = (
    nm_airspaces.query(f"designator in {centres}")
    .replace("LFMMRAW", "Aix-en-Provence (Sud-Est)")
    .replace("LFMMRAE", "Aix-en-Provence (Sud-Est)")
    .replace("LFBBBDX", "Bordeaux (Sud-Ouest)")
    .replace("LFEERMS", "Reims (Est)")
    .replace("LFFFPARIS", "Athis-Mons (Nord)")
    .replace("LFRRBREST", "Brest (Ouest)")
)


chart = (
    alt.hconcat(
        alt.layer(
            alt.Chart(subset.at_level(220))
            .mark_geoshape(stroke="white")
            .encode(
                alt.Tooltip(["designator:N"]),
                alt.Color("designator:N", title="CRNA"),
            ),
            france.mark_geoshape(stroke="#ffffff", strokeWidth=3, fill="#ffffff00"),
            france.mark_geoshape(stroke="#79706e", strokeWidth=1, fill="#ffffff00"),
        ).properties(title="FL 220", width=300),
        alt.layer(
            alt.Chart(subset.at_level(320))
            .mark_geoshape(stroke="white")
            .encode(
                alt.Tooltip(["designator:N"]),
                alt.Color("designator:N", title="CRNA"),
            ),
            france.mark_geoshape(stroke="#ffffff", strokeWidth=3, fill="#ffffff00"),
            france.mark_geoshape(stroke="#79706e", strokeWidth=1, fill="#ffffff00"),
        ).properties(title="FL320", width=300),
    )
    .configure_legend(orient="bottom", labelFontSize=12, titleFontSize=12)
    .configure_view(stroke=None)
    .configure_title(fontSize=16, font="Lato", anchor="start")
)

chart

Iterate through many airspaces

The following trick lets you return the largest CS sector with a designator starting with LFBB (Bordeaux FIR/ACC).

# Find the largest CS in Bordeaux ACC
from operator import attrgetter

max(
    nm_airspaces.query('type == "CS" and designator.str.startswith("LFBB")'),
    key=attrgetter("area"),  # equivalent to `lambda x: x.area`
)
BORDEAUX TOTAL [LFBBBDX] (CS)
  • lower: 145 | upper: 195
  • lower: 195 | upper: 265
  • lower: 265 | upper: inf

Free Route Areas (FRA) from EUROCONTROL

The Free Route information consists of regular airspace information:

from traffic.data import nm_freeroute
nm_freeroute
geometry designator upper lower name type
0 POLYGON ((27.69945 51.47972, 26.79833 51.76639... BEL_FRA 660.0 275.0 FRA
1 POLYGON ((-39.00000 63.50000, -34.25089 65.704... BIRD_DOMESTIC 660.0 0.0 FRA
2 POLYGON ((0.00039 63.01636, 0.00005 72.99989, ... BODO_OCEANIC 660.0 0.0 FRA
3 POLYGON ((18.06361 56.11861, 18.01861 56.09528... BOREALIS 660.0 285.0 FRA
4 POLYGON ((21.00000 59.00000, 20.91583 59.04783... BOREALIS 660.0 95.0 FRA
... ... ... ... ... ... ...
63 POLYGON ((30.53333 43.68333, 29.03333 43.73333... SEE_FRA_SOUTH 660.0 105.0 FRA
64 POLYGON ((-60.00000 65.00000, -59.97538 60.006... SONDRESTROM 660.0 0.0 FRA
65 POLYGON ((29.68778 48.06472, 29.88445 48.19139... UKOV_FRA 660.0 275.0 FRA
66 POLYGON ((27.85000 49.76167, 27.04389 50.18195... UKRNESFRA 660.0 275.0 FRA
67 POLYGON ((25.77750 47.94056, 25.71667 47.93222... UKRNESFRA 660.0 275.0 FRA

68 rows × 6 columns

nm_freeroute["BOREALIS"].map_leaflet(zoom=3)

However, in addition to these airspace, there is a database of points attached to a Free Route Area:

  • I refer to I ntermediate points in the middle of an airspace;

  • E and X mark points of E ntry and e X it;

  • A and D refer to A rrival and D eparture and are attached to an airport.

nm_freeroute.points.query('FRA == "BOREALIS"')
FRA type name latitude longitude airport
5 BOREALIS I TUSRU 59.000000 6.266667 NaN
6 BOREALIS I EVGUN 58.266667 10.000000 NaN
7 BOREALIS EX EVGUN 58.266667 10.000000 NaN
37 BOREALIS I VEKAB 70.250000 29.216667 NaN
41 BOREALIS I NENLA 61.166667 9.333333 NaN
... ... ... ... ... ... ...
2493 BOREALIS D OLNOP 66.188611 24.705278 EFRO
2496 BOREALIS D ARMUV 59.140278 23.466944 EEKA
2508 BOREALIS D SALLO 54.916667 13.386111 EKCH
2508 BOREALIS D SALLO 54.916667 13.386111 EKRK
2508 BOREALIS D SALLO 54.916667 13.386111 ESMS

2399 rows × 6 columns