Introduction

The girths of 30 trees were measured in the Campbell Rhododendron Gardens, Blackheath, NSW. The measurements were made 1.4m from ground level and were recorded to the nearest centimetre. Photographs containing GPS and other ancilliary information were taken of each tree.

Objectives (OB)

The objectives (OB) of this project were to:

  1. create an interactive map using GPS data extracted from the photographs and the Leaflet library, showing the location of each tree with the girth of each tree displayed as a popup.
  2. produce a boxplot and histogram of the girth measurements
  3. provide a quantitative summary table of the girth measurements.

Preparation

Libraries etc

# Clear the environment
rm(list=ls())

library(exiftoolr) # To extract EXIF information from photos; Need to run 'install_exiftoolr()' from R command line after install.packages('exiftoolr')
## Warning: package 'exiftoolr' was built under R version 4.5.1
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(sf) # For making geospatial data frames
## Warning: package 'sf' was built under R version 4.5.1
## Linking to GEOS 3.13.1, GDAL 3.11.0, PROJ 9.6.0; sf_use_s2() is TRUE
library(leaflet) # For making interactive maps
## Warning: package 'leaflet' was built under R version 4.5.1
library(htmltools) # For map popups
library(ggplot2) # For plotting
library(patchwork) # For plot layouts
## Warning: package 'patchwork' was built under R version 4.5.1
library(vtable) # For nicely formatted summary statistics tables
## Warning: package 'vtable' was built under R version 4.5.1
## Loading required package: kableExtra
## Warning: package 'kableExtra' was built under R version 4.5.1
## 
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows

Import and Prepare the Data

Create a list of file names from the photos directory, then read the EXIF data into a data frame.

files <- list.files("./Input/Tree Photos", pattern = "*.JPG")
df_base <- exif_read(paste("./Input/Tree Photos/", files, sep="")) 
## Using ExifTool version 13.32

Merge in the tree girth measurements.

df_base <- read.csv("Input/tree_girths.csv") |> 
                      select(Girth_mm) |>
                      bind_cols(df_base)

Select the desired variables and convert to an sf object.

df_map <- df_base |> 
  select(FileName, DateTimeOriginal, Girth_mm, GPSLatitude, GPSLongitude) |> 
  st_as_sf(coords = c("GPSLongitude","GPSLatitude"), remove = FALSE, crs = 4326)

OB 1

Create An Interactive Map

Create a continuous palette function based on https://rstudio.github.io/leaflet/articles/colors.html with the palette selected from https://colorbrewer2.org/#type=sequential&scheme=RdPu&n=3 .

# Create a continuous palette function
# https://rstudio.github.io/leaflet/articles/colors.html
# https://colorbrewer2.org/#type=sequential&scheme=RdPu&n=3
pal <- colorNumeric(
  palette = "RdPu",
  domain = df_map$Girth_mm)

Create a marker size function. This is required since division used within the radius parameter of addCircleMarkers causes an error.

# Required due to error caused by division when used within 'radius' parameter
size_func <- function(x){x/max(x)*5}

Create the map.

map <- leaflet(elementId = "Trees", 
             data = df_map, # Same data set will be available to all layers
             options = leafletOptions(minZoom = 15, maxZoom = 20)) |>
  
  # Tiles
  addTiles(group = "OSM (default)") |> # Gives Open Street Map tiles 
  addProviderTiles(providers$CartoDB.Positron, group = "Positron (minimal)") |>
  addProviderTiles(providers$Esri.WorldImagery, group = "World Imagery (satellite)") |>
  setView(150.2864, -33.6268, zoom = 18) |>
  
  # Markers
  addCircleMarkers(popup = ~htmlEscape(paste("Girth: ", Girth_mm, "mm", sep = "")), 
                   # radius = ~sqrt(Girth_mm)*0.1, 
                   radius = ~size_func(Girth_mm), 
                   color = ~pal(Girth_mm),
                   group = "Trees") |>
  
  # Controls
   addLayersControl(
    baseGroups = c(
      "OSM (default)",
      "Positron (minimal)",
      "World Imagery (satellite)"
    ),
    overlayGroups = c("Trees"),
    options = layersControlOptions(collapsed = FALSE)) |>

  # Legend
   addLegend("bottomright", pal = pal, values = ~Girth_mm,
    title = "Girth (mm) at 1.4m",
    labFormat = labelFormat(suffix = "mm"),
    opacity = 1)


map

Comments

The extraction of the EXIF data from the photographs was straight forward, although it would be time consuming for a large number of files. Using Leaflet to create the map was also relatively uncomplicated and has produced a framework that could easily be adapted to other mapping projects. The main drawback of this method is inaccuracy in the GPS positioning. The trees appear to be positioned correctly relative to each other, although they are up to several metres off relative to landmarks such as the road to the west of the tree cluster. This inaccuracy would be due in part to the method of standing back from the trees when taking the photographs, since the position of the camera is recorded and not the position of the tree. This error could be overcome by photographing from the base of the tree. For example, a photo could be taken up into the tree’s canopy from it’s base, thus giving a more accurate GPS record of the tree’s position.

OB 2

Boxplot of Girths

# fig.height controls height of plot in knitted document
# https://stackoverflow.com/a/39634521/8299958

df_map %>%
  ggplot(aes(y=Girth_mm)) +
  geom_boxplot() +
  scale_x_discrete() +
  labs(title="Boxplot of Tree Girths", y="Girth (mm) at 1.4m")

Histogram of Girths

Histograms were plotted with 5, 7, and 9 bins for comparison.

# fig.height controls height of plot in knitted document
# https://stackoverflow.com/a/39634521/8299958

plots <- lapply(c(5,7,9), FUN = function(x){
  df_map %>%
  ggplot(aes(x=Girth_mm)) +
  geom_histogram(bins=x) +
  ggtitle("Histogram of Tree Girths", paste("Bins = ", x, sep="")) +
  labs(x="Girth (mm) at 1.4m", y="Frequency")
})

# https://patchwork.data-imaginist.com/articles/guides/layout.html
# Plot height more easily controlled using fig.height in chunk specifications
plots[[1]] + plots[[2]] + plots[[3]] 

# +
# plot_layout(nrow=1, heights = unit(c(8), c("cm")))

The boxplot and histograms suggest the distribution of girths is slightly positively skewed, with a mean in the range [1200mm, 1300mm].

OB 3

Summary Table of Girth Statistics

sumtable(df_map[,'Girth_mm'],
         summ = c('notNA(x)',
                  'countNA(x)',
                  'propNA(x)',
                  'mean(x)',   
                  'sd(x)', 
                  'min(x)', 
                  'pctile(x)[25]', 
                  'median(x)',
                  'pctile(x)[75]', 
                  'max(x)'),
         summ.names = list(
               c('N',
                 'NA',
                 'propNA',
                 'Mean',
                 'SD',
                 'Min',
                 'Q1',
                 'Median',
                 'Q2',
                 'Max')
                  )
      )
Summary Statistics
Variable N NA propNA Mean SD Min Q1 Median Q2 Max
Girth_mm 30 0 0 1327 511 450 938 1330 1580 2410

The summary table confirms that the mean girth is 1327mm, almost equal to the median (1330mm). The maximum girth (2410mm) is approximately 5.4 times that of the minimum (450mm). There is a comparatively large standard deviation (511mm), giving a coefficient of variation (CV) equal to \(1327/511=2.60\).