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:
- a defunct airfield (formerly Norfolk / Heenan-Menfi Memorial, from 1940’s to late 1990’s)
- Mansfield Municipal (1B9)
- North Central State (KSFZ)
- Hopedale Industrial Park (1B6)
- the intersection between two highways
- the western border of Framingham
- back to the defunct airfield and Norwood (KOWD).
Flight plan on a chart to Video tour in Google Earth
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
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 dataframegeosphere
, to calculate distances and headings between pairs of latitude / longitude
Show the code
library(tidyverse)
library(XML)
library(geosphere)
##### Setup #####
<- "kowd_tour.fpl"
input_file <- "kowd_tour"
output_name <- "Tour of KOWD Practice Area"
name
<- paste0(output_name, ".Rda")
output_base_file <- paste0(output_name, ".kml")
output_file
<- 2500 # in feet
altitude_default <- "absolute" # absolute --> MSL; relativeToGround --> AGL, at low altitudes can be bumpier
altitude_mode <- 5 # 10 degrees looks natural; 15 - 20 degrees can see more ground; tilt = 90 - degrees_below_horizon
degrees_below_horizon
<- 0.5 # 0.5 is gentle at 2500' AGL
miles_per_second <- 20 # for turns to heading, 20°/second is gentle
degrees_per_second <- 0 # 2 seconds is good
time_before_rotate
##### Functions #####
<- function(lon, lat, lon_next, lat_next) {
calculate_bearing ::bearing(c(lon, lat), c(lon_next, lat_next))
geosphere
}
<- function(lon, lat, lon_next, lat_next) {
calculate_distance ::distVincentyEllipsoid(c(lon, lat), c(lon_next, lat_next)) / 1852 # 1852 meters in 1 nautical mile
geosphere
}
##### Read FPL file as XML #####
<- xmlRoot(xmlParse(file = input_file))
rootnode <- rootnode[[1]][[1]] # not used
created_date <- xmlToDataFrame(rootnode[[2]])
df_waypoint <- xmlToDataFrame(rootnode[[3]]) # not used
df_route
<- df_waypoint %>% # Can alter altitudes in df_base
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_base %>%
df 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',
%>% pull(flyto) %>% paste(collapse = "\n"),
df '\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
(the marker flags visible during the video are from personal Google Earth bookmarks; it could be possible to add the
identifier
labels into the exportedKML
file)↩︎