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.
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.
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”).
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)
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.
tracking_data<- track2KBA::boobies
Before going further, let’s look into the data a bit more. This code will help show some basic information about your dataset.
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>
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.
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"
track_years<- year(dataGroup$date_gmt)
unique(track_years)
## [1] 2012 2013 2014
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)
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)
trackssf <- st_as_sf(tracks_df, coords = c("Longitude","Latitude"), crs = 4326)
map_extent<-st_bbox(trackssf)
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)
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)
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.
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
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}')
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)
Then save to your directory using the anim_save() function.
anim_save("Track2KBA_Boobies_69302_20241609.gif", animation)
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]