De Knegt and colleagues studied habitat selection by African elephants in Kruger National Park (KNP), South Africa’s largest nature reserve, covering roughly 19,000 km2 and harbouring close to 14,000 African elephants (Loxodonta africana). 33 elephants (19 females and 14 males) were tagged with GPS collars. Locations were recorded at hourly intervals over a three‐year period (2005–2008). In this report, I show how R can be used to decipher and the coded GPS records and be visualized on a map, using a subset of the data of a subset of individuals.

The dataset that I used contains tracking data from 2 individuals, with 798 fixes in total. Each fix consists of 3 variables: one number (column timestamp) and two strings (columns id and payload).

Datetime objects are often stored in a UNIX timestamp format: a number that represents the number of seconds that passed since midnight of January 1, 1970, GMT time. With the package lubridate, these numbers can easily be converted into readable dates and times. Here we save them as new variable dttm.

dat <- dat %>%
  mutate(dttm = parse_date_time("1970-1-1 0:0:00", 
                                orders = "%Y-%m-%d %H:%M:%S",
                                tz = "GMT") + timestamp)
dat
## # A tibble: 798 x 4
##    id     timestamp payload            dttm               
##    <chr>      <dbl> <chr>              <dttm>             
##  1 am72  1151064476 017202f979afdb4cfb 2006-06-23 12:07:56
##  2 am72  1151066276 016e02f9b55fdb508b 2006-06-23 12:37:56
##  3 am72  1151068076 016302f9bd1fdb512e 2006-06-23 13:07:56
##  4 am72  1151069876 015f02f9cc5fdb50c6 2006-06-23 13:37:56
##  5 am72  1151071675 015002f9c98fdb508e 2006-06-23 14:07:55
##  6 am72  1151073475 014102f9cc9fdb5065 2006-06-23 14:37:55
##  7 am72  1151075275 012b02f9ccbfdb509b 2006-06-23 15:07:55
##  8 am72  1151077075 011902f9b40fdb51e7 2006-06-23 15:37:55
##  9 am72  1151078875 010602f9a6afdb5360 2006-06-23 16:07:55
## 10 am72  1151080675 00f702f9a2ffdb53be 2006-06-23 16:37:55
## # ... with 788 more rows

The payload is a text string composed of a series of codes, called nibbles.

  1. The first 4 nibbles codes for the ambient temperature * 10
  2. The second 7 nibbles codes for the GPS longitude * 1e5
  3. The last 7 nibbles code for the GPS latitude * 1e5

Using the str_sub function, we can break up the payload string as follows:

dat <- dat %>%
  mutate(temp_hex = str_sub(payload, start = 1,  end = 4),
         lon_hex  = str_sub(payload, start = 5,  end = 11),
         lat_hex  = str_sub(payload, start = 12, end = 18)) %>%
  select(-c(timestamp,payload))
dat
## # A tibble: 798 x 5
##    id    dttm                temp_hex lon_hex lat_hex
##    <chr> <dttm>              <chr>    <chr>   <chr>  
##  1 am72  2006-06-23 12:07:56 0172     02f979a fdb4cfb
##  2 am72  2006-06-23 12:37:56 016e     02f9b55 fdb508b
##  3 am72  2006-06-23 13:07:56 0163     02f9bd1 fdb512e
##  4 am72  2006-06-23 13:37:56 015f     02f9cc5 fdb50c6
##  5 am72  2006-06-23 14:07:55 0150     02f9c98 fdb508e
##  6 am72  2006-06-23 14:37:55 0141     02f9cc9 fdb5065
##  7 am72  2006-06-23 15:07:55 012b     02f9ccb fdb509b
##  8 am72  2006-06-23 15:37:55 0119     02f9b40 fdb51e7
##  9 am72  2006-06-23 16:07:55 0106     02f9a6a fdb5360
## 10 am72  2006-06-23 16:37:55 00f7     02f9a2f fdb53be
## # ... with 788 more rows

Then, to make clear that these are not numeric data, we add the prefix 0x, as follows.

dat <- dat %>%
  mutate(temp_hex = str_c("0x",temp_hex,sep=""),
         lon_hex  = str_c("0x",lon_hex,sep=""),
         lat_hex  = str_c("0x",lat_hex,sep=""))
dat
## # A tibble: 798 x 5
##    id    dttm                temp_hex lon_hex   lat_hex  
##    <chr> <dttm>              <chr>    <chr>     <chr>    
##  1 am72  2006-06-23 12:07:56 0x0172   0x02f979a 0xfdb4cfb
##  2 am72  2006-06-23 12:37:56 0x016e   0x02f9b55 0xfdb508b
##  3 am72  2006-06-23 13:07:56 0x0163   0x02f9bd1 0xfdb512e
##  4 am72  2006-06-23 13:37:56 0x015f   0x02f9cc5 0xfdb50c6
##  5 am72  2006-06-23 14:07:55 0x0150   0x02f9c98 0xfdb508e
##  6 am72  2006-06-23 14:37:55 0x0141   0x02f9cc9 0xfdb5065
##  7 am72  2006-06-23 15:07:55 0x012b   0x02f9ccb 0xfdb509b
##  8 am72  2006-06-23 15:37:55 0x0119   0x02f9b40 0xfdb51e7
##  9 am72  2006-06-23 16:07:55 0x0106   0x02f9a6a 0xfdb5360
## 10 am72  2006-06-23 16:37:55 0x00f7   0x02f9a2f 0xfdb53be
## # ... with 788 more rows

The last step consists of converting the hexadecimal codes to integers and divide these by 1e5, to obtain latitude and longitude.

dat <- dat %>%
  mutate(lon = map_dbl(lon_hex, hex2integer) / 1e5,
         lat = map_dbl(lat_hex, hex2integer) / 1e5)
dat
## # A tibble: 798 x 7
##    id    dttm                temp_hex lon_hex   lat_hex     lon   lat
##    <chr> <dttm>              <chr>    <chr>     <chr>     <dbl> <dbl>
##  1 am72  2006-06-23 12:07:56 0x0172   0x02f979a 0xfdb4cfb  31.2 -24.1
##  2 am72  2006-06-23 12:37:56 0x016e   0x02f9b55 0xfdb508b  31.2 -24.0
##  3 am72  2006-06-23 13:07:56 0x0163   0x02f9bd1 0xfdb512e  31.2 -24.0
##  4 am72  2006-06-23 13:37:56 0x015f   0x02f9cc5 0xfdb50c6  31.2 -24.0
##  5 am72  2006-06-23 14:07:55 0x0150   0x02f9c98 0xfdb508e  31.2 -24.0
##  6 am72  2006-06-23 14:37:55 0x0141   0x02f9cc9 0xfdb5065  31.2 -24.0
##  7 am72  2006-06-23 15:07:55 0x012b   0x02f9ccb 0xfdb509b  31.2 -24.0
##  8 am72  2006-06-23 15:37:55 0x0119   0x02f9b40 0xfdb51e7  31.2 -24.0
##  9 am72  2006-06-23 16:07:55 0x0106   0x02f9a6a 0xfdb5360  31.2 -24.0
## 10 am72  2006-06-23 16:37:55 0x00f7   0x02f9a2f 0xfdb53be  31.2 -24.0
## # ... with 788 more rows

Now that we have converted the data from hexadecimal representation into decimal representation, we can plot the elephant trajectories on a dynamic leaflet map, plotting a separate line for each individual. We will also show different base layers: the default open-streetmap layer, as well as the ESRI world imagery data. We will add a menu where you can toggle the individuals as well as the base layer.

library(leaflet)
leaflet(dat) %>%
  addTiles(group = "default") %>%
  addTiles(urlTemplate = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png",
         attribution = "ESRI world imagery",
         group = "ESRI world imagery") %>%
  # Add separate lines
  addPolylines(lng = ~lon, lat = ~lat, color = "#ff0000", group = "am72",
               data = filter(dat, id == "am72")) %>%
  addPolylines(lng = ~lon, lat = ~lat, color = "#0000ff", group = "am160",
               data = filter(dat, id == "am160")) %>%
  # Layers control
  addLayersControl(
    baseGroups = c("default", "ESRI world imagery"),
    overlayGroups = c("am72", "am160"),
    options = layersControlOptions(collapsed = FALSE)
  )

The map shows …