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.
The objectives (OB) of this project were to:
sf and leaflet.leaflet examples.# 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
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)
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
# 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")
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].
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')
)
)
| 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\).
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.