Skip to content

Commit

Permalink
Merge pull request ocean-tracking-network#82 from ocean-tracking-netw…
Browse files Browse the repository at this point in the history
…ork/new_canssi_material

Added new material from CANSSI workshop.
  • Loading branch information
jackVanish authored Nov 16, 2023
2 parents 1fe0c23 + 79de13c commit e7ae6b5
Show file tree
Hide file tree
Showing 9 changed files with 535 additions and 42 deletions.
Binary file added Resources/GAMs.pptx
Binary file not shown.
Binary file added Resources/YAPS.pptx
Binary file not shown.
52 changes: 27 additions & 25 deletions _episodes/12-basic-animation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ questions:
- "How do I animate my animal movements?"
---

Static plots are excellent tools and are appropriate a lot of the time, but there are instances where something extra is needed to demonstrate your interesting fish movements: This is where plotting animated tracks can be a useful tool. In this lesson we will explore how to take data from your [OTN-style detection extract documents](https://members.oceantrack.org/data/otn-detection-extract-documentation-matched-to-animals) and animate the journey of one fish between stations.
Static plots are excellent tools and are appropriate a lot of the time, but there are instances where something extra is needed to demonstrate your interesting fish movements. This is where plotting animated tracks can be a useful tool. In this lesson we will explore how to take data from your [OTN-style detection extract documents](https://members.oceantrack.org/data/otn-detection-extract-documentation-matched-to-animals) and animate the journey of one fish between stations.

### Getting our Packages

Expand All @@ -33,10 +33,12 @@ For the purposes of this lesson we will assume that any detection that did not p
Finally, we use the `detection_events` function with station as the `location_col` argument to get our detection events.

~~~
unzip('nsbs_matched_detections_2022.zip', overwrite = TRUE)
detection_events <- #create detections event variable
read_otn_detections('proj58_matched_detections_2016.csv') %>% # reading detections
read_otn_detections('nsbs_matched_detections_2022/nsbs_matched_detections_2022.csv') %>%
false_detections(tf = 3600) %>% #find false detections
filter(passed_filter != FALSE) %>%
dplyr::filter(passed_filter != FALSE) %>%
detection_events(location_col = 'station', time_sep=3600)
~~~
{: .language-r}
Expand All @@ -49,10 +51,10 @@ plot_data <- detection_events %>%
~~~
{: .language-r}

Additionally, animating many animal tracks can be computationally intensive as well as create a potentially confusing plot, so for this lesson we will only be plotting one fish. We well subset our data by filtering where the `animal_id` is equal to `PROJ58-1218508-2015-10-13`.
Additionally, animating many animal tracks can be computationally intensive as well as create a potentially confusing plot, so for this lesson we will only be plotting one fish. We well subset our data by filtering where the `animal_id` is equal to `NSBS-1393342-2021-08-10`.

~~~
one_fish <- plot_data[plot_data$animal_id == "PROJ58-1218508-2015-10-13",]
one_fish <- plot_data[plot_data$animal_id == "NSBS-1393342-2021-08-10",]
~~~
{: .language-r}

Expand All @@ -61,7 +63,7 @@ one_fish <- plot_data[plot_data$animal_id == "PROJ58-1218508-2015-10-13",]

Now that we have our data we can begin to create our plot. We will start with creating a static plot and then once happy with that, we will animate it.

The first thing we will do for our plot is download the basemap, this will be the background to our plot. To do this we will use the `get_stamenmap` function from `ggmap`. This function gets a Stamen Map based off a bounding box that we provide. To create the bounding box we will pass a vector of four values to the argument `bbox` ; those four values represent the left, bottom, right, and top boundaries of the map.
The first thing we will do for our plot is download the basemap. This will provide the background for our plot. To do this we will use the `get_stadiamap` function from `ggmap`. This function gets a Stamen Map based off a bounding box that we provide. "Stamen" is the name of the service that provides the map tiles, but it was recently bought by Stadia, so the name of the function has changed. To create the bounding box we will pass a vector of four values to the argument `bbox` ; those four values represent the left, bottom, right, and top boundaries of the map.

To determine which values are needed we will use the `min` and `max` function on the `mean_longitude` and `mean_latitude` columns of our `one_fish` variable. `min(one_fish$mean_longitude)` will be our left-most bound, `min(one_fish$mean_latitude)` will be our bottom bound, `max(one_fish$mean_longitude)` will be our right-most bound, and `max(one_fish$mean_latitude)` will be our top bound. This gives most of what we need for our basemap but we can further customize our plot with `maptype` which will change what type of map we use, `crop` which will crop raw map tiles to the specified bounding box, and `zoom` which will adjust the zoom level of the map.

Expand All @@ -76,55 +78,55 @@ To determine which values are needed we will use the `min` and `max` function on

~~~
basemap <-
get_stamenmap(
get_stadiamap(
bbox = c(left = min(one_fish$mean_longitude),
bottom = min(one_fish$mean_latitude),
right = max(one_fish$mean_longitude),
top = max(one_fish$mean_latitude)),
maptype = "toner-lite",
maptype = "stamen_toner_lite",
crop = FALSE,
zoom = 8)
zoom = 7)
ggmap(basemap)
~~~
{: .language-r}

Now that we have our basemap ready we can create our static plot. We will store our plot in a variable called `act.plot` so we can access it later on.
Now that we have our basemap ready we can create our static plot. We will store our plot in a variable called `otn.plot` so we can access it later on.

To start our plot we will call the `ggmap` function and pass it our basemap as an argument. To make our detection locations we will then call `geom_point`, supplying `one_fish` as the data argument and for the aesthetic will make the `x` argument equal to `mean_longitude` and the `y` argument will be `mean_latitude`.
To start our plot we will call the `ggmap` function and pass it our basemap as an argument. To make our detection locations we will then call `geom_point`, supplying `one_fish` as the data argument. For the aesthetic we will make the `x` argument equal to `mean_longitude` and the `y` argument will be `mean_latitude`. This will orient our map and data properly.

We will then call `geom_path` to connect those detections supplying `one_fish` as the data argument and for the aesthetic `x` will again be `mean_longitude` and `y` will be `mean_latitude`.
We will then call `geom_path` to connect those detections supplying `one_fish` as the data argument. The aesthetic `x` will again be `mean_longitude` and `y` will be `mean_latitude`.

Lastly, we will use the `labs` function to add context to our plot such as adding a `title`, a label for the `x` axis, and a label for the `y` axis. We are then ready to view our graph by calling `ggplotly` with `act.plot` as the argument!
Lastly, we will use the `labs` function to add context to our plot including a `title`, a label for the `x` axis, and a label for the `y` axis. We are then ready to view our graph by calling `ggplotly` with `otn.plot` as the argument!

~~~
act.plot <-
ggmap(base) +
geom_point(data = one_fish2, aes(x = mean_longitude, y = mean_latitude, group = animal_id, color = animal_id), size = 2) +
geom_path(data = one_fish2, aes(x = mean_longitude, y = mean_latitude, group = animal_id, color = animal_id)) +
labs(title = "ACT animation",
otn.plot <-
ggmap(basemap) +
geom_point(data = one_fish, aes(x = mean_longitude, y = mean_latitude), size = 2) +
geom_path(data = one_fish, aes(x = mean_longitude, y = mean_latitude)) +
labs(title = "NSBS Animation",
x = "Longitude", y = "Latitude", color = "Tag ID")
ggplotly(act.plot)
ggplotly(otn.plot)
~~~
{: .language-r}

### Animating our Static Plot

Once we have a static plot we are happy with we are ready for the final step of animating it! We will use the `gganimate` package for this, since it integrates nicely with `ggmap`.
Once we have a static plot we are happy with, we are ready for the final step of animating it! We will use the `gganimate` package for this, since it integrates nicely with `ggmap`.

To animate our plot we update our `act.plot` variable by using it as our base, then add a label for the dates to go along with the animation. We then call `transition_reveal`, which is a function from `gganimate` that determines how to create the transitions for the animations. There are many transitions you can use for animations with `gganimate` but `transition_reveal` will calculate intermediary values between time observations. For our plot we will pass `transition_reveal` the `first_detection` information. We will finally use the functions `shadow_mark` with the arguments of `past` equal to `TRUE` and `future` equal to `FALSE`. This makes the animation continually show the previous data (a track) but not the future data yet to be seen (allowing it to be revealed as the animation progresses).
To animate our plot we update our `otn.plot` variable by using it as our base, then add a label for the dates to go along with the animation. We then call `transition_reveal`, which is a function from `gganimate` that determines how to create the transitions for the animations. There are many transitions you can use for animations with `gganimate` but `transition_reveal` will calculate intermediary values between time observations. For our plot we will pass `transition_reveal` the `first_detection` information. We will finally use the functions `shadow_mark` with the arguments of `past` equal to `TRUE` and `future` equal to `FALSE`. This makes the animation continually show the previous data (a track) but not the future data yet to be seen (allowing it to be revealed as the animation progresses).

Finally, to see our new animation we call the `animate` function with `act.plot` as the argument.
Finally, to see our new animation we call the `animate` function with `otn.plot` as the argument.

~~~
act.plot <-
act.plot +
otn.plot <-
otn.plot +
labs(subtitle = 'Date: {format(frame_along, "%d %b %Y")}') +
transition_reveal(first_detection) +
shadow_mark(past = TRUE, future = FALSE)
animate(act.plot)
animate(otn.plot)
~~~
{: .language-r}

53 changes: 36 additions & 17 deletions _episodes/13-animation-with-pathroutr.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,51 @@ library(pathroutr)
library(ggspatial)
library(sp)
library(raster)
library(geodata)
detection_events <- #create detections event variable
read_otn_detections('proj58_matched_detections_2016.csv') %>% # reading detections
read_otn_detections('nsbs_matched_detections_2022.csv') %>% # reading detections
false_detections(tf = 3600) %>% #find false detections
filter(passed_filter != FALSE) %>%
dplyr::filter(passed_filter != FALSE) %>%
detection_events(location_col = 'station', time_sep=3600)
plot_data <- detection_events %>%
dplyr::select(animal_id, mean_longitude,mean_latitude, first_detection)
one_fish <- plot_data[plot_data$animal_id == "PROJ58-1218518-2015-09-16",]
one_fish <- plot_data[plot_data$animal_id == "NSBS-1393342-2021-08-10",]
one_fish <- one_fish %>% filter(mean_latitude < 38.90 & mean_latitude > 38.87) %>%
slice(155:160)
~~~
{: .language-r}

There is one small tweak we are going to make that is not immediately intuitive, and which we're only doing for the sake of this lesson. The blue sharks in our dataset have not given us many opportunities to demonstrate `pathroutr`'s awareness of coastlines. In order to give you a fuller demonstration of the package, we are going to cheat and shift the data 0.5 degrees to the west, which brings it more into contact with the Nova Scotian coastline and lets us show off `pathroutr` more completely. You do not need to do this with your real data.

~~~
one_fish_shifted <- one_fish %>% mutate(mean_longitude_shifted = mean_longitude-0.5)
~~~
{: .language-r}

### Getting our Shapefile

The first big difference between our basic animation lesson and this lesson is that we will need a shapefile of the study area, so `pathroutr` can determine where the landmasses are located. To do this we will use the `getData` function from the `raster` library which gets geographic data for anywhere in the world. The first argument we will pass to `getData` is the name of the dataset we want to use, for our visualization we will use `GADM`. When you use `GADM` with `getData` you need to provide a `country` argument, which is specified by a country's 3 letter ISO code. In our case we will pass `USA`. Last, we pass an argument for `level`, this is how much we would like the data to be subdivided. Since we are only interested in a portion of the country we will pass `level` the value of `1` which means that the data will be divided by states. When we run this code we will get back a `SpatialPolygonsDataFrame` with the shapefiles for the US states.
The first big difference between our basic animation lesson and this lesson is that we will need a shapefile of the study area, so `pathroutr` can determine where the landmasses are located. To do this we will use the `gadm` function from the `geodata` library which gets administrative boundaries (i.e, borders) for anywhere in the world. The first argument we will pass to `gadm` is the name of the country we wish to get, in this case, Canada. We will specify `level` as 1, meaning we want our data to be subdivided at the first level after 'country' (in this case, provinces). 0 would get us a single shapefile of the entire country; 1 will get us individual shapefiles of each province. We must also provide a path for the downloaded shapes to be stored (`./geodata` here), and optionally a resolution. `gadm` only has two possible values for resolution: 1 for 'high' and 2 for 'low'. We'll use low resolution here because as we will see, for this plot it is good enough and will reduce the size of the data objects we download.

This is only one way to get a shapefile for our coastlines- you may find you prefer a different method. Regardless, this is the one we'll use for now.

~~~
USA<-getData('GADM', country='USA', level=1)
CAN<-gadm('CANADA', level=1, path="./geodata", resolution=2)
~~~
{: .language-r}

Since we only need one state we will have to filter out the states we don't need. We can do this by filtering the data frame using the same filtering methods we have explored in previous lessons.
We only need one province, which we can select using the filtering methods common to R.

~~~
shape_file <- USA[USA$NAME_1 == 'Maryland',]
shape_file <- CAN[CAN$NAME_1 == 'Nova Scotia',]
~~~
{: .language-r}

This shapefile is a great start, but we need the format to be an `sf` `multipolygon`. To do that we will run the `st_as_sf` function on our shapefile. We also want to change the coordinate reference system (CRS) of the file to a projected coordinate system since we will be mapping this plot flat. To do that we will run `st_transform` and provide it the value `5070`.

~~~
md_polygon <- st_as_sf(single_poly) %>% st_transform(5070)
ns_polygon <- st_as_sf(single_poly) %>% st_transform(5070)
~~~
{: .language-r}

Expand All @@ -73,7 +80,7 @@ We will also need to make some changes to our detection data as well, in order t
Using the `SpatialPoints` function we will pass our new `path` variable and `CRS("+proj=longlat +datum=WGS84 +no_defs")` for the `proj4string` argument. Just like for our shapefile we will need to turn our path into an `sf` object by using the `st_as_sf` function and change the CRS to a projected coordinate system because we will be mapping it flat.

~~~
path <- one_fish %>% dplyr::select(mean_longitude,mean_latitude)
path <- one_fish_shifted %>% dplyr::select(mean_longitude_shifted,mean_latitude)
path <- SpatialPoints(path, proj4string = CRS("+proj=longlat +datum=WGS84 +no_defs"))
Expand All @@ -86,7 +93,7 @@ We can do a quick plot to just check how things look at this stage and see if th

~~~
ggplot() +
ggspatial::annotation_spatial(md_polygon, fill = "cornsilk3", size = 0) +
ggspatial::annotation_spatial(ns_polygon, fill = "cornsilk3", size = 0) +
geom_point(data = path, aes(x=unlist(map(geometry,1)), y=unlist(map(geometry,2)))) +
geom_path(data = path, aes(x=unlist(map(geometry,1)), y=unlist(map(geometry,2)))) +
theme_void()
Expand All @@ -107,14 +114,14 @@ track_pts <- st_sample(plot_path, size = 10000, type = "regular")
The first `pathroutr` function we will use is `prt_visgraph`. This creates a visibility graph that connects all of the vertices for our shapefile with a Delaunay triangle mesh and removes any edges that cross land. You could think of this part as creating the viable routes an animal could swim through (marking the "water" as viable).

~~~
vis_graph <- prt_visgraph(md_polygon, buffer = 150)
vis_graph <- prt_visgraph(ns_polygon, buffer = 100)
~~~
{: .language-r}

To reroute our paths around the landmasses we will call the `prt_reroute` function. Passing `track_pts`, `md_polygon`, and `vis_graph` as arguments. To have a fully updated path we can run the `prt_update_points` function, passing our new path `track_pts_fix` with our old path `track_pts`.

~~~
track_pts_fix <- prt_reroute(track_pts, land_barrier, vis_graph, blend = TRUE)
track_pts_fix <- prt_reroute(track_pts, ns_polygon, vis_graph, blend = TRUE)
track_pts_fix <- prt_update_points(track_pts_fix, track_pts)
~~~
Expand All @@ -126,7 +133,7 @@ For `geom_point` and `geom_path` we will pass in `track_pts_fix` for the `data`

~~~
pathroutrplot <- ggplot() +
ggspatial::annotation_spatial(md_polygon, fill = "cornsilk3", size = 0) +
ggspatial::annotation_spatial(ns_polygon, fill = "cornsilk3", size = 0) +
geom_point(data = track_pts_fix, aes(x=unlist(map(geometry,1)), y=unlist(map(geometry,2)))) +
geom_path(data = track_pts_fix, aes(x=unlist(map(geometry,1)), y=unlist(map(geometry,2)))) +
theme_void()
Expand All @@ -145,6 +152,18 @@ pathroutrplot.animation <-
transition_reveal(fid) +
shadow_mark(past = TRUE, future = FALSE)
gganimate::animate(pathroutrplot.animation)
gganimate::animate(pathroutrplot.animation, nframes=100, detail=2)
~~~
{: .language-r}
{: .language-r}

> ## A Note on Detail
>
> You'll note that the animation we've generated still crosses the landmass at certain points. This is a combination of several factors: our coastline polygon is not very high-res, our animation does not have many frames, and what frames it does have are not rendered in great detail. We can increase all of these and get a more accurate plot. For example:
> - We can specify `resolution=1` when downloading our shapefile from GADM.
> - We can increase the `nframes` variable in our call to `gganimate::animate`.
> - We can pass `detail = 2` or higher to the call to `gganimate::animate`.
>
> All of these will give us an animation that more scrupulously respects the landmass, however, they will all bloat the runtime of the code significantly. This may not be a consideration when you create your own animations, but they do make it impractical for this workshop.
> Embedded below is an animation created with high-resolution polygons and animation parameters to show an example of the kind of animation we could create with more time and processing power.
> ![High-resolution Pathroutr animation](../files/highres_pathroutr.gif)
{: .callout}
Loading

0 comments on commit e7ae6b5

Please sign in to comment.