Switching Flight Historian to ICAO Regions

(updated )

Early on during the development of Flight Historian, I realized that I’d have to do some filtering of my maps by region. Most of my travel is within the United States, so a world map of all of my flights left the United States as an unreadable mess of lines. Thus, I gave Flight Historian the ability to toggle between world maps (all flights) and CONUS maps (flights within the CONtiguous United States—that is, the United States except for Alaska, Hawaii, and territories).

I use Great Circle Mapper to generate my flight maps, and because of peculiarities with how it generates maps, showing region maps wasn’t as simple as setting a map center and zoom level. Instead, I had to know which airports were inside the CONUS and which ones were outside. The easiest solution was to add an is_conus attribute to the airports table in my database, which would be set to true for CONUS airports and false for OCONUS (Outside CONUS) airports. Once I had that, I could set the world map to use every airport, and the CONUS map to show only airports where is_conus was true.

This worked well enough when I was only showing two regions (world and CONUS). But as I traveled, I realized I was going to want to zoom in on other regions (for example, Europe) as well, which meant that I’d have to have some way match airports to other regions.

One option would have been to add an is_europe flag to my Airports table as well, but that would have quickly gotten unwieldy with having to add a column for every additional region.

I thought about creating a region text field, where I could just specify for each airport whether it was CONUS, Europe, or Other. However, this would also greatly limit me—it could be possible in the future that I might want to have airports that are in multiple regions (for example, if I ever wanted to show a map of the entire US including Alaska and Hawaii, then the CONUS airports would be in both the CONUS region and the Entire US region).

In order to create a many-to-many relationship of airports to regions, it looked like I was going to have to create a Regions table to list all my potential regions, and then a Regions_Airports table which would let me associate regions and airports with each other. Any time I created a new region, I’d have to go through all of my airports and decide which airports were in that region.

And then I realized that the ICAO had already done that work for me.

ICAO Codes

When most people think of airport codes, they think of the three letter codes like LAX or LHR that you see on your boarding pass. These are technically called IATA aircraft codes, which are generally (but not always) related to the name of the city or airport they’re assigned to.

However, any airport you’re likely to fly to will also have an ICAO code, which is a four letter code that is much more rigorous in what the letters mean. Of particular interest, the first few letters of the ICAO code tell you where in the world the airport is!

World map, showing the first letter of the ICAO code for each region.

By Hytar (CC BY-SA 3.0), via Wikimedia Commons

The first letter of the ICAO code tells you where in the world the airport is. For example, all CONUS airports start with a K, and all Canadian airports start with a C. Northern Europe begins with an E, southern Europe begins with an L, and airports in many Pacific regions and islands start with a P.

Once you’re in a particular region, the second letter will often specify what country the airport is in (Great Britain codes start with EG, Alaska with PA, and Hawaii with PH).

This isn’t always true—regions that belong entirely to a single country may not break it down (for example, most airports in the US have an ICAO code of KZZZ, where ZZZ is the IATA code—thus LAX becomes KLAX, ORD becomes KORD, and so on.)

So if I added the ICAO code to each of my airports, I could use that to define any region I wanted, by just finding any airports whose ICAO codes started with certain letters! CONUS would be any airports starting with K. The whole USA would be airports starting with K, PH, or PA. Europe (including Iceland and Greenland) would start with B, E, or L. Airports in the United Kingdom only would be EG.


Once I added an icao_code field to the Airports model, it was easy to write a function which would take an array of region start strings (e.g., ["B","E","L"]) and return a hash of airport IDs and IATA codes:

# Take a collection of strings representing the starts of ICAO codes, and
# return an a hash of airports in the region, with airport ids as keys and
# IATA codes as values.
# Params:
# +icao_starts+:: An array of strings of the start of ICAO codes (i.e. EG, K)
def self.in_region_hash(icao_starts)
  # Build a querystring of LIKE statements with question marks connected by ORs:
  conditions = icao_starts.map{"icao_code LIKE ?"}.join(" OR ")
  # Create an array of patterns to insert as parameters:
  patterns = icao_starts.map{|start| "#{start}%"}
  # Run the SQL query, expanding the patterns to insert at the question marks:
  matching_airports = Airport.where(conditions, *patterns)
  # Create a hash of airport IDs and IATA codes:
  iata_hash = Hash.new
    |airport| iata_hash[airport[:id]] = airport[:iata_code]
  return iata_hash

From there, instead of building my CONUS maps based on the airports returned by Airport.where(is_conus: true), I could instead build my maps based on which airports were returned by Airport.in_region_hash(["K"]). Additionally, I could specify any other region(s) I wanted as an argument to build a map based on those regions, and I could specify an array containing an empty string ([""]) to show the entire world (since every ICAO code matches WHERE icao_code LIKE %).


Ultimately, it turned out that there was no need to manually specify airport regions when ICAO had already done the work. Most maps now offer the option to toggle between World, CONUS, and Europe if there are any flights in those regions, and it will be trivial for me to add more regions in the future if I travel to other parts of the world.