From flight plan to Google Earth video tour

Using R to convert a .FPL flight plan into a Google Earth video tour
Flight
R
Published

July 17, 2024

Terminal Area Chart

Google Earth

As someone with very little sense of direction, the idea of pilotage for cross-country flights is slightly terrifying.

Pilotage is navigation by reference to landmarks or checkpoints. It is a method of navigation that can be used on any course that has adequate checkpoints … The checkpoints selected should be prominent features common to the area of the flight. (FAA Handbook of Aeronautical Knowledge)

Yes, there’s GPS (and I will use it), but I still think developing skills in pilotage is important.

Here I describe how to go from a flight plan plotted on a chart to a Google Earth tour video, with the goal to help familiarize myself with prominent visual references along the way.

Aeronautical charts

On the left above is a terminal area chart (TAC), oriented north upwards, with shows labels and flags for good visual landmarks, including “Walpole Prison” and “Stadium” (Gillette Stadium). Just above Walpole prison is a thin black line with an icon showing that it represents power lines.

On the right is how it looks in real life, looking to the south. There are actually two prisons visible (square and walled), the stadium in the upper left corner, and the horizontal stripe at the bottom of the picture representing cleared areas for the power lines.

What’s a good way to get from one to the other?

Flight plans

There are also some really great websites that combine the aeronautical charts with the ability to create flight plans; e.g. SkyVector.

From within SkyVector, it is very easy to create a series of waypoints, connected into a flight plan, complete with analysis of winds aloft, magnetic variation, to calculate route headings accounting for wind drift, distances, time between points, etc.

Here’s a flight plan generated in SkyVector, starting from Norwood Memorial Airport (KOWD), flying west, and then clockwise circling the practice area to a number of landmarks including:

Flight plan on a chart to Video tour in Google Earth

  1. SkyVector lets you export the flight plan in a number of formats, including the XML-based .FPL format used by Garmin, and includes all the latitude / longitude waypoints

  2. Google Earth lets you create video tours, based on a set of waypoints, with really nice high resolutiom satellite imagery.

Hmmmm… I think I know where I’m going with this…

Google Earth issues

Google Earth seems to be intended to give very aesthetically pleasing, smooth video playback, with nice curving paths and interpolations. Usually, the points of interest are things you want to look at, not representing where you want to look from.

My goal to to use flight plan waypoints is to look from each waypoint precisely in the direction of the next waypoint, so that during the straight line segment paths, I can use Google Earth imagery to see exactly what I should see if I followed the course perfectly.

Turns out Google Earth has great documentation on their “Keyhole Markup Language” (KML) that makes this easy to achieve. The most relevant for my goal included Touring in KML and Cameras.

Goal

  • start with a Garmin FPL / XML file of waypoints
  • start with the camera at a waypoint, pointing directly at the next waypoint
  • move in a straight line to the next waypoint
  • on arrival, rotate the camera to look at the following waypoint
  • repeat for all waypoints

Additionally:

  • move at a constant ground speed – scale the duration of each movement to the distance travelled
  • rotate the camera at a constant angular speed during camera heading changes
  • set the camera altitude – using altitude above mean sea level (MSL) worked better than above ground level (AGL) for a smoother ride
  • set camera tilt relative to the horizon – pointing at the horizon is more realistic, but seeing more ground is helpful
  • export a KML file that contains a complete tour

Full R code below

R Package requirements were minimal:

  • tidyverse
  • XML, to parse the XML into a dataframe
  • geosphere, to calculate distances and headings between pairs of latitude / longitude
Show the code
library(tidyverse)
library(XML)
library(geosphere)

##### Setup #####
input_file <- "kowd_tour.fpl"
output_name <- "kowd_tour"
name <- "Tour of KOWD Practice Area"

output_base_file <- paste0(output_name, ".Rda")
output_file <- paste0(output_name, ".kml")

altitude_default <- 2500 # in feet
altitude_mode <- "absolute" # absolute --> MSL; relativeToGround --> AGL, at low altitudes can be bumpier
degrees_below_horizon <- 5 # 10 degrees looks natural; 15 - 20 degrees can see more ground; tilt = 90 - degrees_below_horizon

miles_per_second <- 0.5 # 0.5 is gentle at 2500' AGL
degrees_per_second <- 20 # for turns to heading, 20°/second is gentle
time_before_rotate <- 0  # 2 seconds is good

##### Functions #####
calculate_bearing <- function(lon, lat, lon_next, lat_next) {
  geosphere::bearing(c(lon, lat), c(lon_next, lat_next))
}

calculate_distance <- function(lon, lat, lon_next, lat_next) {
  geosphere::distVincentyEllipsoid(c(lon, lat), c(lon_next, lat_next)) / 1852 # 1852 meters in 1 nautical mile
}

##### Read FPL file as XML #####
rootnode <- xmlRoot(xmlParse(file = input_file))
created_date <- rootnode[[1]][[1]] # not used
df_waypoint <- xmlToDataFrame(rootnode[[2]])
df_route <- xmlToDataFrame(rootnode[[3]]) # not used

df_base <- df_waypoint %>% # Can alter altitudes in df_base
  transmute(
    identifier,
    lat = as.numeric(lat),
    lon = as.numeric(lon),
    lat_next = lead(lat),
    lon_next = lead(lon),
    altitude = altitude_default
  ) %>% 
  rowwise() %>%
  mutate(
    track = calculate_bearing(lon, lat, lon_next, lat_next) %% 360,
    distance = calculate_distance(lon, lat, lon_next, lat_next)
  ) %>%
  ungroup() %>% 
  select(-lat_next, -lon_next)

Here’s the source dataframe df_base of waypoints. If desired, you can manually tweak the altitudes for specific waypoints here.

# A tibble: 9 × 6
  identifier   lat   lon altitude track distance
  <chr>      <dbl> <dbl>    <dbl> <dbl>    <dbl>
1 KOWD        42.2 -71.2     2500 246.      9.76
2 SV001       42.1 -71.4     2500 134.     10.8 
3 1B9         42.0 -71.2     2500 250.     14.0 
4 KSFZ        41.9 -71.5     2500 356.     11.2 
5 1B6         42.1 -71.5     2500 347.     11.3 
6 SV002       42.3 -71.6     2500  95.1     5.37
7 SV003       42.3 -71.4     2500 161.      9.90
8 SV004       42.1 -71.4     2500  66.6     9.77
9 KOWD        42.2 -71.2     2500  NA      NA   

Next, we generate all the camera movement and rotate views needed and embed them in FlyTo tour commands.

Show the code
##### Generate camera views and flyto commands #####
df <- df_base %>% 
  mutate(
    track = ifelse(is.na(track), lag(track), track), # the final point will just keep same last heading
    track_last = ifelse(is.na(lag(track)), track, lag(track)), # the first point will start by pointing to second point
    heading_change = abs(track - track_last) %% 180
  ) %>% 
  mutate(
    # Hacky camera duplication: only difference is track, so camera doesn't rotate until reaches destination
    camera1 = paste0(
      '<Camera id = "', identifier, '">', 
      "<longitude>", lon, "</longitude>",
      "<latitude>", lat, "</latitude>",
      "<altitude>", round(altitude / 3.28084), "</altitude>", # convert feet to meters
      "<heading>", round(track_last, 2), "</heading>",
      "<tilt>", 90 - degrees_below_horizon, "</tilt>",
      "<roll>", 0, "</roll>",
      "<altitudeMode>", altitude_mode, "</altitudeMode>",
      "</Camera>"
    ),
    camera2 = paste0(
      '<Camera id = "', identifier, '">', 
      "<longitude>", lon, "</longitude>",
      "<latitude>", lat, "</latitude>",
      "<altitude>", round(altitude / 3.28084), "</altitude>",
      "<heading>", round(track, 2), "</heading>",
      "<tilt>", 90 - degrees_below_horizon, "</tilt>",
      "<roll>", 0, "</roll>",
      "<altitudeMode>", altitude_mode, "</altitudeMode>",
      "</Camera>"
    )
  ) %>% 
  mutate(
    flyto = paste0(
      
      # Smoothly fly camera from each point to next, with duration proportional to distance to next point
      "<gx:FlyTo><gx:flyToMode>smooth</gx:flyToMode>", # don't bounce
      "<gx:duration>", ifelse(is.na(lag(distance)), 0, round(lag(distance) / miles_per_second, 1)), "</gx:duration>",
      camera1,
      "</gx:FlyTo>",
      
      # Wait before rotating camera to new heading
      "<gx:Wait><gx:duration>", time_before_rotate, "</gx:duration></gx:Wait>",
      
      # Rotate camera to new heading, with duration proportional to amount of heading change
      "<gx:FlyTo><gx:flyToMode>bounce</gx:flyToMode>", # rotate either smoothly or with bounce acceleration / deceleration
      "<gx:duration>", round(heading_change / degrees_per_second, 1), "</gx:duration>", # this amount of time for rotating camera to new heading
      camera2,
      "</gx:FlyTo>"
    )
  )

Finally, generate the Google Earth Tour and save it in a KML file. Double-clicking the KML file immediately opens Google Earth and starts playing the tour.

Show the code
##### Generate and save KML tour file #####
tour <-
  paste0(
  '<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2"
 xmlns:gx="http://www.google.com/kml/ext/2.2">

<Document>
  <name>', name,'</name>
  <open>1</open>

  <gx:Tour>
    <name>Play tour</name>
    <gx:Playlist>\n\n',
         df %>% pull(flyto) %>% paste(collapse  = "\n"),
    '\n\n    </gx:Playlist>
  </gx:Tour>

</Document>
</kml>'
  )


##### Save KML file output #####

# write(tour, output_file)

Link to the KML file – save and open with Google Earth (app seems better than web site).

Follow the flight from KOWD to the practice area, going clockwise1

The very cool thing is that when running in Google Earth, you can pause at any point to change the camera view and look around. This will be great to look for additional landmarks and orientation. When done exploring, you can continue the tour where you left off.

I think this is really cool.

Footnotes

  1. (the marker flags visible during the video are from personal Google Earth bookmarks; it could be possible to add the identifier labels into the exported KML file)↩︎