Seabird Tracking Animation Exercise

Animating your tracking data enables you to produce a live map of animal movements. This is a usefull tool when screening, analysing and visualising your data. The exercise below will use seabird data as an example of how to further investigate tracking data with animation.

Setting Up Your Computer

1. Download R studio

If you don’t already have R & RStudio running on your computer, you will have to download the software for this exercise. R is free and easily accessbile software that can be dowloaded here.

2. Set Up Your Working Directory

This can be selected in the “session” tab in RStudio, but it can also be set up with the setwd() function by using a file path (eg. “H:/tutorial-animation”).

3. Packages to Install in R studio

Below is a list of all packages needed for the following exercise. If you do not already have access to these, use the install.packages() function for each. Then the library() function allows you to load packages you have installed into your R environment.

library(sf)
library(lubridate)
library(gifski)
library(magick)
library(png)
library(gganimate)
library(ggmap)
library(track2KBA)
library(tidyverse)

4. Load in Tracking Data

For the purpose of this exercise, tracking data will be used from the Track2KBA package. Boobie data can be loaded in from BirdLife’s Track2KBA package. This exercise will also utilise the trip splitting steps for the Track2KBA package in R as is layed out in the Track2KBA tutorial.

The link to the Track2KBA tutorial can be found here.

Load in seabird data into your environment

tracking_data<- track2KBA::boobies

Investigate Data

Before going further, let’s look into the data a bit more. This code will help show some basic information about your dataset.

1. Split all tracking data into data gropus

dataGroup <- formatFields(
  dataGroup = tracking_data, 
  fieldID   = "track_id", 
  fieldDate = "date_gmt", 
  fieldTime = "time_gmt",
  fieldLon  = "longitude", 
  fieldLat  = "latitude"
)
## No format supplied for Date and Time fields, a default format ('ymd_HMS')
##       attempted when combining the fields. If error produced, see help page
##       ('?lubridate::parse_date_time') for information on date formats.
# here we know that the first points in the data set are from the colony center
colony <- dataGroup %>% 
  summarise(
    Longitude = first(Longitude), 
    Latitude  = first(Latitude)
  )

Then view your output.

str(dataGroup)
## 'data.frame':    178006 obs. of  8 variables:
##  $ ID        : chr  "69302" "69302" "69302" "69302" ...
##  $ date_gmt  : chr  "2012-07-22" "2012-07-22" "2012-07-21" "2012-07-21" ...
##  $ time_gmt  : chr  "04:17:15" "04:28:57" "13:04:33" "13:18:24" ...
##  $ Longitude : num  -5.73 -5.73 -5.73 -5.73 -5.73 ...
##  $ Latitude  : num  -16 -16 -16 -16 -16 ...
##  $ lon_colony: num  -5.73 -5.73 -5.73 -5.73 -5.73 -5.73 -5.73 -5.73 -5.73 -5.73 ...
##  $ lat_colony: num  -16 -16 -16 -16 -16 ...
##  $ DateTime  : POSIXct, format: "2012-07-22 04:17:15" "2012-07-22 04:28:57" ...
##  - attr(*, ".internal.selfref")=<externalptr>

2. Take a look at your data

When dealing with a new dataset you may have a number of key questions you would like to investigate about your data. Below are examples of information you can quickly pull from your dataset in RStudio.

How many track IDs do we have?
unique(dataGroup$ID)
##  [1] "69302" "69303" "69304" "69305" "69306" "69307" "69308" "69309" "69310"
## [10] "69311" "69312" "69313" "69314" "69315" "69316" "69317" "69318" "69319"
## [19] "69320" "69321" "69322" "69323" "69324" "69325" "69326" "69327" "69328"
## [28] "69329" "69330" "69331" "69332" "69333" "69334" "69335" "69336" "69337"
## [37] "69338" "69339" "69340" "69341" "69342" "69343" "69344"
Over how many years does the data occur?
track_years<-  year(dataGroup$date_gmt) 
unique(track_years)
## [1] 2012 2013 2014
Are they all complete tracks?

The code below allows you to split tracking data for each ID into individual trips. It also indicates which trips were complete (ie. returned to the colony)

trips <- tripSplit(
  dataGroup  = dataGroup,
  colony     = colony,
  innerBuff  = 3,      # kilometers
  returnBuff = 10,
  duration   = 1,      # hours
  rmNonTrip  = TRUE
)
## track 693041 does not return to the colony
## track 693434 does not return to the colony

There are too many trips to view all completed vs not completed trips, but the mapTrips() function will give you a preview of the first 25 trips.

mapTrips(trips = trips, colony = colony)

Let’s just see the incomplete trips.

incomplete_trips<-  subset(trips, trips$Returns == "No" )
mapTrips(trips = incomplete_trips , colony = colony)

3. Organise data

First, selected only the completed trips from the dataset.

complete_trips <- subset(trips, trips$Returns == "Yes" )

We know this data spans over a number of years, so let’s extract one year to look at. Add a column with the year information to your dataset, then select only the rows from one particular year.

complete_trips<-  as.data.frame(complete_trips)%>%
  mutate(year = year(date_gmt)) 


year_2012_df  <- complete_trips %>%
                filter(year== 2012)

make sure your date&time column are in a POSIXct format.

year_2012_df$datetime <- as.POSIXct(paste(year_2012_df$date_gmt, year_2012_df$time_gmt), format="%Y-%m-%d %H:%M:%S")

make sure your data is in the correct order, organise by bird ID then by date & time.

tracks_df<- year_2012_df%>%
          group_by(ID,
           datetime)

Map Data

1. Convert your data into an ‘sf’ object and get the extent for your map

trackssf <- st_as_sf(tracks_df, coords = c("Longitude","Latitude"), crs = 4326)
map_extent<-st_bbox(trackssf)

2. Convert to a dataframe

Create a dataframe from your spatial object, keeping relevant columns for your animation.

Get co-ordinates from your sf object.

tracksgeo <- st_coordinates(trackssf)

Convert to data frame and rename Latitude & Longitude columns

tracksgeo <- as.data.frame(tracksgeo)
colnames(tracksgeo) <- c("X", "Y")

Include relevant columns and make sure each is in the correct format (POSIXct for date and time, a factor for bird ids, as a date for date information)

tracksgeo$id <- as.factor(tracks_df$ID)

tracksgeo$date_time <- as.POSIXct(tracks_df$DateTime, format="%Y-%m-%d %H:%M:%S")
head(tracks_df, 3)
## # A tibble: 3 × 18
## # Groups:   ID, datetime [3]
##   ID    date_gmt   time_gmt Longitude Latitude lon_colony lat_colony
##   <chr> <chr>      <chr>        <dbl>    <dbl>      <dbl>      <dbl>
## 1 69302 2012-07-22 07:52:11     -5.69    -16.0      -5.73      -16.0
## 2 69302 2012-07-22 07:53:50     -5.68    -16.0      -5.73      -16.0
## 3 69302 2012-07-22 07:55:29     -5.67    -16.0      -5.73      -16.0
## # ℹ 11 more variables: DateTime <dttm>, tripID <chr>, X <dbl>, Y <dbl>,
## #   Returns <chr>, StartsOut <chr>, ColDist <dbl>, dataGroup.Longitude <dbl>,
## #   dataGroup.Latitude <dbl>, year <dbl>, datetime <dttm>
tracksgeo$date <- as.Date(tracks_df$date_gmt)

3. Create Basemap

Here you will create a basemap for your animation output & static maps using ggmap.

For this step you will need to register on Stadia maps, directions for this can be found here.

Using your map extent from earlier, create a basemap within the bounds of your tracking data.

print(map_extent)
##      xmin      ymin      xmax      ymax 
##  -5.70904 -16.14960  -4.93102 -15.81775

I round up/down the map_extent values to create a bbox that is just slightly bigger that the are covered by the tracks.

map_bbox<- c (left=-6, bottom=-16.2, right= -4.5, top=-15.5)
mybasemap <- get_stadiamap(bbox = map_bbox, zoom = 1)

Now, plot your basemap.

ggmap(mybasemap)

4. Plot your seabird tracks on your basemap

Here you will create a static map of the seabird tracks which you can then animate using ggplot. First, you need to plot your track data points onto your ggmap basemap.

Here we will group by id (as well as facet wrap with id values) & the point colours are based on bird ID. However, you can use other values for grouping or colours, depending on your dataset and what you are looking at. These include;

-male/female,

-juvenile/mature,

-breeding/non-breeding

mymap.paths <- ggmap(mybasemap) + 
  geom_point(data = tracksgeo, aes(x = X, y = Y, colour = id)) +
  geom_path(data = tracksgeo, aes(x = X, y = Y, colour = id, group=id)) +
  labs(x = "longitude", y = "latitude") +
  scale_colour_manual(name = "Bird ID",
                      values = rainbow(length(unique(tracksgeo$id))),
                      breaks = unique(tracksgeo$id)) + 
  theme(legend.position = "bottom") +
  facet_wrap(~id)

Now, plot your static map of the tracks from 2012.

Animate Data

Using your existing static plot, you should now be able to animate the tracks of the seabirds in your selected year. Create an animation path using your static plot, then display the animation with a set speed. You can save this as a gif to your working directory.

Further information can be found on tutorials for gganimate example 1 ; example 2

1. Create a path.animate.plot

Here you add transition time() to your static plot. As this data runs over a rumber of days date_time information is used to set the transition of the points. Shadow_wake() is a function that allows you to edit the “shadow”/line trailing behind your points. This can be a particularly useful function when dealing with more than one track in an animation. The labs function allows you to include the date_time data if you wish this to be displayed during the animation (use ’{frame_time}), or you can also create a set label. More information about the different functions for animations can be found here.

path.animate.plot <- mymap.paths +
  transition_time(date_time) +
shadow_wake(
  wake_length=0.3,
  size = TRUE,
  alpha = TRUE,
  colour = 'grey92',
  fill = NULL,
  falloff = "cubic-in",
  wrap = TRUE,
  exclude_layer = NULL,
  exclude_phase = c("enter", "exit")
) +
  labs(title = 'Example Boobie GPS Tracking: {frame_time}')

2. Display the animated plot

Use animate() function and animation path to produce the live map. Specify the speed of the animation. The slower you make the animation, the longer it will take to render. Likewise, the more tracks you have the longer the animation will take to render.

animation<-animate(path.animate.plot, fps = 5)

3. Save as GIF

Then save to your directory using the anim_save() function.

anim_save("Track2KBA_Boobies_69302_20241609.gif", animation)

References

Beal, M., Oppel, S., Handley, J., Pearmain, E. J., Morera-Pujol, V., Carneiro, A. P. B., Davies, T. E., Phillips, R. A., Taylor, P. R., Miller, M. G. R., Franco, A. M. A., Catry, I., Patrício, A. R., Regalla, A., Staniland, I., Boyd, C., Catry, P., & Dias, M. P. (2021). track2KBA: An R package for identifying important sites for biodiversity from tracking data. Methods in Ecology and Evolution, 12(12), 2372-2378. [https://doi.org/10.1111/2041-210X.13713]

Kahle D, Wickham H (2013). “ggmap: Spatial Visualization with ggplot2.” The R Journal, 5(1), 144–161. [https://journal.r-project.org/archive/2013-1/kahle-wickham.pdf]

Oppel, S., Beard, A., Fox, D., Mackley, E., Leat, E., Henry, L., Clingham, E., Fowler, N., Sim, J., Sommerfeld, J., Weber, N., Weber, S., Bolton, M., 2015. Foraging distribution of a tropical seabird supports Ashmole’s hypothesis of population regulation. Behav Ecol Sociobiol 69, 915–926. [https://doi.org/10.1007/s00265-015-1903-3]

Pebesma E (2018). “Simple Features for R: Standardized Support for Spatial Vector Data.” The R Journal, 10(1), 439–446. [https://doi.org/10.32614/RJ-2018-009]

Pedersen T, Robinson D (2024). gganimate: A Grammar of Animated Graphics. R package version 1.0.9.9000, [https://github.com/thomasp85/gganimate]

Wickham H, Averick M, Bryan J, Chang W, McGowan LD, François R, Grolemund G, Hayes A, Henry L, Hester J, Kuhn M, Pedersen TL, Miller E, Bache SM, Müller K, Ooms J, Robinson D, Seidel DP, Spinu V, Takahashi K, Vaughan D, Wilke C, Woo K, Yutani H (2019). “Welcome to the tidyverse.” Journal of Open Source Software, 4(43), 1686.[https://doi.org/10.21105/joss.01686]