Fan Symbols

Author

K. W. Bridges

Published

October 23, 2022

A “fan” is a graphic symbol that shows a location, direction and field width. Combined with the other graphic symbols, the fan places photos in a rich context on base maps.

The basic information for a fan symbol is added to a basic data table or it can be in its own data table. This distinction is important as it is often possible to generate a data table from a set of photographs. The site_photos function builds a table with values you can use to create the fields necessary for the site_fans function. You’ll see how this works in a few examples.

Cellphones collect considerable data about photos being taken. These data values are stored in each photo’s EXIF data which is part of a JPG (or JPEG) image. This shows an advantage of using cellphone photos to document sites. Many “regular” cameras do not store all of the information needed to create fan symbols.

It is possible, although perhaps tedious, to add data if a set of photos doesn’t have location, direction or view width information. In that case, the values used may be general approximation. These are often adequate for the sorts of maps produced with the sitemaps functions.

Getting Started

We’ll get started by doing our usual initialization.

Show the code chunk
library(geosphere)
library(sitemaps)
library(ggmap)
library(ggplot2)
library(readr)
library(gt)
library(dplyr)
library(sp)     ## To make the SpatialPoints
library(maptools)   ## Used? Going to be retired at end of 2023
library(tibble)
library(stringr)
library(exifr)        ## Get photo EXIF data
library(lubridate)    ## Handle dates
library(stats)
library(REAT)         ## Used to do the regression for the field of view
library(exifr)        ## Photo EXIF data retrieval

## Initialize Google Map key; the key is stored in a Project directory.
  My_Key <- read_file("P://Hot/Workflow/Workflow/keys/Google_Maps_API_Key.txt")

## Test if Google Key is registered.
if (!has_google_key()){

  ## Register the Google Maps API Key.
  register_google(key = My_Key, account_type = "standard")
  } ## end Google Key test

A few more things are need before we begin.

Show the code chunk
## Use two functions from sitemaps to initialize parameters
column <- site_styles()
hide   <- site_google_hides()

## Establish a theme that improves the appearance of a map.
## This theme removes the axis labels and 
## puts a border around the map. No legend.
simple_black_box <- theme_void() +
              theme(panel.border = element_rect(color = "black", 
                                   fill=NA, 
                                   size=2),
                    legend.position = "none")

Example: Map photos using default values

This example shows the locations and direction information for a few photos taken with a cellphone at the National Arboretum (Washington, DC).

All of the data (Table 1) for the map come from an EXIF extraction using the site_photo function. A few columns needed renaming so that they will plot using the site_fans and site_labels functions.

The purpose of this example is to show the relative ease of providing a map with photo information with very little code.

Warning

The dir and fov data in the photo EXIF file may not be accurate, especially if the camera compass is not calibrated. It is possible to manually update the EXIF data using exiftool with the exiftool GUI software. Consult exiftool.org for the software and editing information.

Show the code chunk
## Reset the defaults
column <- site_styles()

## Photo folder
folder <- "photos/NatArboretum"

## Read the EXIF data
exif_data <- site_photos(folder)

## Print the data for confirmation
gt(exif_data) %>% 
  fmt_number(columns = c(lat,lon), decimals = 5) %>% 
  fmt_number(columns = fov, decimals = 0) 
Table 1: Image information for photos from the National Arboretum.
number file description lat lon dir fov dist date time
1 PXL_20220404_154844253.jpg Headquarters 38.91195 −76.97059 20 74 3 2022-04-04 11:48
2 PXL_20220404_163353453.jpg Bonsai Garden 38.91241 −76.96875 80 97 3 2022-04-04 12:33
3 PXL_20220404_165324797.jpg Herb Garden 38.91193 −76.96950 170 74 3 2022-04-04 12:53
4 PXL_20220404_165931864.jpg Cherry Tree 38.91141 −76.96876 273 74 3 2022-04-04 12:59
5 PXL_20220404_170448577.jpg Old Capitol Pillars 38.91056 −76.96815 104 74 3 2022-04-04 13:04
6 PXL_20220404_175915228.jpg Azalea Garden 38.90844 −76.97099 350 74 3 2022-04-04 13:59
7 PXL_20220404_180736978.jpg Capitol Pillar Head 38.91008 −76.97033 87 20 3 2022-04-04 14:07

The photo file has been processed and returned relevant information. However, the site_photos function purposely returns column names that are not correct for processing with the site_fans function. This is done because it isn’t always obvious which columns will be used and, quite possibly, how they should be modified before they are used.

The next code chunk shows a simple renaming of the key columns and the production of a map (Figure 1) with the fan symbols.

Show the code chunk
## Save a new dataset with column names updated so they are standard.
exif_data2 <- exif_data %>% 
  dplyr::rename(text            = description, 
                fan_direction   = dir, 
                fan_field_width = fov)

## Adjust the basemap margin
column$margin <- 0.8

## Basemap
basemap <- site_google_basemap(datatable = exif_data2)

## Put on data points, labels and arms
fan_test <- ggmap(basemap) +
  site_fans(datatable   = exif_data2) +
  site_points(datatable = exif_data2) +
  site_labels(datatable = exif_data2) +
  simple_black_box

fan_test

Figure 1: Fan symbols for photos taken at the National Arboretum.

Example: Adding direction data to photo locations

Some cell phones (and maybe cameras) provide both the direction (bearing) and view width (FOV) for each photo with this information saved in the EXIF data.

This example, photo information only had the lat/lon data. This was copied into the data table. Reference to a Google Maps image provided an estimation of the direction each photo was taken. Table 2 provides another test of the sites_fan function.

Show the code chunk
## Reset the parameters
column <- site_styles()

## The data file with the photo direction information
direction_data <- read_csv(col_names = TRUE, file =
 "text, lat,       lon,         fan_direction
103652, 34.126966, -118.110479,  85
104012, 34.126753, -118.109852, 187
104924, 34.126059, -118.110084, 309
105052, 34.126086, -118.110165, 273
105513, 34.125702, -118.110084, 160
110451, 34.125009, -118.110411,  95
110848, 34.125142, -118.110639,  25
111201, 34.125277, -118.110960, 301")

## Put out a table to confirm the EXIF data
gt(direction_data) %>% 
  fmt_number(columns = c(lat,lon), decimals = 5)
Table 2: Image information for photos from the Huntington Botanical Garden.
text lat lon fan_direction
103652 34.12697 −118.11048 85
104012 34.12675 −118.10985 187
104924 34.12606 −118.11008 309
105052 34.12609 −118.11016 273
105513 34.12570 −118.11008 160
110451 34.12501 −118.11041 95
110848 34.12514 −118.11064 25
111201 34.12528 −118.11096 301

The data table already has the correct names for the site_fans and site_labels functions. Now it is time to do a plot on a satellite image (Figure 2).

The code chunk has a commented-out statement that shows how to get a PNG file with the map.

Show the code chunk
# ## Rename some columns to the standard names
# photo_exif2 <- photo_exif %>% 
#   dplyr::rename(text=number, fan_direction=dir, fan_field_width=fov)

## Adjust some of the fan styles
column$fan_alpha <- 0.4
column$fan_arm_length <- 50
column$margin <- 0.9

## Assign a field width based on the camera information (lens length, etc)
## This is the same for all the photos
column$fan_field_width <- 48

## Basemap
column$gmaptype <- "satellite"
basemap <- site_google_basemap(datatable = direction_data)

## Put on data points, labels and arms
plot_hunt <- ggmap(basemap) +
  site_fans(datatable   = direction_data) +
  site_points(datatable = direction_data) +
  site_labels(datatable = direction_data) +
  simple_black_box

plot_hunt

## ggsave(filename="Huntington_photolocs.png",plot=plot_hunt, width=8, height=8)

Figure 2: Photos in the desert area at the Huntington Botanical Garden

Example: Fan symbols and point colors

This example is a reminder that the other sitemap functions can be used with the fan symbols.

We’ll use site_fans function with the Puhimau map to show the locations, directions and field of view of each of the photos taken at this research site along with the a color code to indicate the temperature category. The analysis starts with just the location and temperature data (Table 3).

Show the code chunk
## Read in the field data.
puhimau <- read_csv(col_names = TRUE, file = 
      "Site,  Lon,        Lat,        Temperature, color
       1,    -155.251451,   19.389354,  140,         red
       2,    -155.251483,   19.388421,  125,         orange
       3,    -155.249834,   19.389709,  165,         red
       4,    -155.248924,   19.389591,  180,         red
       5,    -155.248982,   19.388827,  102,         blue
       6,    -155.248024,   19.389952,   97,         blue
       7,    -155.249994,   19.388816,  135,         orange
       8,    -155.251831,   19.387395,   95,         blue")

## Rename the a few column and shift all the column names to lowercase
puhimau2 <- puhimau %>% 
  dplyr::rename(text = Site, point_color = color) %>% 
  dplyr::rename_with(tolower)

## Show the data in a table
gt(puhimau2) %>% 
  fmt_number(columns = c(lat,lon), decimals = 5) %>% 
  tab_source_note(source_note = "Data: 2017 field survey") %>% 
  tab_footnote(footnote = "degrees F",
      locations = cells_column_labels(columns = temperature))
Table 3: Surface soil temperature measurements at Puhimau Hot Spot.
text lon lat temperature1 point_color
1 −155.25145 19.38935 140 red
2 −155.25148 19.38842 125 orange
3 −155.24983 19.38971 165 red
4 −155.24892 19.38959 180 red
5 −155.24898 19.38883 102 blue
6 −155.24802 19.38995 97 blue
7 −155.24999 19.38882 135 orange
8 −155.25183 19.38740 95 blue
Data: 2017 field survey
1 degrees F

The next thing is to add some data specifically for the fans. The two key items are the fan_direction (bearing) of the photo and the fan_field_width (field of view).

A new data table is constructed that has the fan information. The tables are merged with the column that is the same in both tables (“text”). As usual, it is a good idea to check that all is well (Table 4) before moving on.

Show the code chunk
## Read the extra data about the photos
photo_dir <- read_csv(col_names = TRUE, file = 
  "text, fan_direction, fan_field_width
   1,    45,        53
   2,    75,        120
   3,    210,       53
   4,    160,       75
   5,    10,        95
   6,    190,       75
   7,    45,        45
   8,    25,        40")

## Merge the two sets of data
puhimau3 <- merge(puhimau2, 
                        photo_dir, 
                        by="text")  

## Make a table to verify the data
gt(puhimau3) %>% 
  fmt_number(columns = c(lat,lon), decimals = 5)
Table 4: Full data for the Puhimau photo locations.
text lon lat temperature point_color fan_direction fan_field_width
1 −155.25145 19.38935 140 red 45 53
2 −155.25148 19.38842 125 orange 75 120
3 −155.24983 19.38971 165 red 210 53
4 −155.24892 19.38959 180 red 160 75
5 −155.24898 19.38883 102 blue 10 95
6 −155.24802 19.38995 97 blue 190 75
7 −155.24999 19.38882 135 orange 45 45
8 −155.25183 19.38740 95 blue 25 40

We’re now ready to place the fan symbols on the map (Figure 3). Note that a few of the default values for the fan have been changed.

Show the code chunk
## Modify the arm characteristics & don't fill the fan
column$fan_arm_length <- 80
column$fan_arm_color  <- "red"
column$fan_fill       <- FALSE

## Modify the label connector color so that it isn't confused with the arms
column$label_connector_color <- "white"

## Obtain a basemap
column$gmaptype <- "satellite"
basemap <- site_google_basemap(datatable = puhimau3)

## Put on data points, labels and fan arms
puhi_map <- ggmap(basemap) +
  site_fans(datatable = puhimau3) +
  site_labels(datatable = puhimau3) +
  site_points(datatable = puhimau3)

puhi_map

Figure 3: Photo locations and view areas at Puhimau Hotspot.

Calculating the Field of View

The field of view of a camera, which is used as the fan_field_width, depends on the focal length of the lens and the sensor size. For the discussion here, let’s assume we are working with full-frame (or equivalent) camera sensors.

Data relating the field of view to the lens focal length is given at the www.nikonians.org/reviews/fov-tables website. The data from this website are used here to develop a function that can be used to calculate the field of view from the focal length.

The function is:

field of view <- 1088.15264 * focal_length ^ -0.95814

The function works pretty well except as the shortest focal lengths. Nonetheless, this is probably adequate for the diagrams produced by site_fans.

The code a function that can be used to calculate the horizontal field of view for any lens using the focal length (mm).

Show the code chunk
## Field of View data
fov <- read_csv(col_names = TRUE, file =
"focal_length, horiz_fov
 13, 81.2
 17, 70.4
 24, 53.1
 35, 37.8
 50, 27.0
 85, 16.1
 90, 15.2
100, 13.5
135, 10.2
200,  6.9
400,  3.4
600,  2.3
800,  1.7")

## Get the fit to several popular regression models
parms <- curvefit(fov$focal_length, fov$horiz_fov, plot.curves = FALSE)
Curve fitting 

                       a            b Std. Error a Std. Error b   t value a
Linear        38.9727451 -0.065847661   7.85888900 0.0253613157   4.9590655
Power       1088.1526430 -0.958140163   0.02523186 0.0123547203 120.3514143
Exponential   34.5382982 -0.004531752   0.21212657 0.0006845508  16.6979022
Logistic      -0.5874711  0.007211284   0.81927038 0.0026438565  -0.7170662
             t value b   Pr(>|t|) a   Pr(>|t|) b R squared Adj. R squared
Linear       -2.596382 4.294222e-04 2.485382e-02 0.3799743      0.3236083
Power       -77.552558 1.630833e-18 2.039202e-16 0.9981744      0.9980084
Exponential  -6.620038 3.668037e-09 3.760363e-05 0.7993612      0.7811213
Logistic      2.727563 4.882802e-01 1.966383e-02 0.4034577      0.3492266
                F value       Pr(>F)
Linear         6.741199 2.485382e-02
Power       6014.399260 2.039202e-16
Exponential   43.824899 3.760363e-05
Logistic       7.439598 1.966383e-02
Show the code chunk
## The power model works best
parms  ## look at power model
$models_comp
                       a            b Std. Error a Std. Error b   t value a
Linear        38.9727451 -0.065847661   7.85888900 0.0253613157   4.9590655
Power       1088.1526430 -0.958140163   0.02523186 0.0123547203 120.3514143
Exponential   34.5382982 -0.004531752   0.21212657 0.0006845508  16.6979022
Logistic      -0.5874711  0.007211284   0.81927038 0.0026438565  -0.7170662
             t value b   Pr(>|t|) a   Pr(>|t|) b R squared Adj. R squared
Linear       -2.596382 4.294222e-04 2.485382e-02 0.3799743      0.3236083
Power       -77.552558 1.630833e-18 2.039202e-16 0.9981744      0.9980084
Exponential  -6.620038 3.668037e-09 3.760363e-05 0.7993612      0.7811213
Logistic      2.727563 4.882802e-01 1.966383e-02 0.4034577      0.3492266
                F value       Pr(>F)
Linear         6.741199 2.485382e-02
Power       6014.399260 2.039202e-16
Exponential   43.824899 3.760363e-05
Logistic       7.439598 1.966383e-02

$models_y
        x    y   ypred_lin ypred_pow  ypred_exp ypred_logi
 [1,]  13 81.2  38.1167256 93.191426 32.5623274 50.4742767
 [2,]  17 70.4  37.8533349 72.068800 31.9773875 49.9205583
 [3,]  24 53.1  37.3924013 51.790964 30.9789129 48.9428657
 [4,]  35 37.8  36.6680770 36.079143 29.4724954 47.3864490
 [5,]  50 27.0  35.6803621 25.635301 27.5356405 45.2317341
 [6,]  85 16.1  33.3756940 15.418285 23.4969318 40.1227029
 [7,]  90 15.2  33.0464557 14.596596 22.9705071 39.3903921
 [8,] 100 13.5  32.3879791 13.195003 21.9527753 37.9286790
 [9,] 135 10.2  30.0833110  9.897636 18.7329169 32.8919003
[10,] 200  6.9  25.8032130  6.791732 13.9533320 24.2567407
[11,] 400  3.4  12.6336809  3.495841  5.6370894  7.4265162
[12,] 600  2.3  -0.5358512  2.370454  2.2773612  1.8872538
[13,] 800  1.7 -13.7053833  1.799379  0.9200447  0.4541876
Show the code chunk
## Power model parameters
fov$fit_fov <- 1088.15264*fov$focal_length^-0.95814

## Check results with a table
gt(fov)
focal_length horiz_fov fit_fov
13 81.2 93.191464
17 70.4 72.068833
24 53.1 51.790991
35 37.8 36.079164
50 27.0 25.635317
85 16.1 15.418296
90 15.2 14.596607
100 13.5 13.195013
135 10.2 9.897644
200 6.9 6.791738
400 3.4 3.495844
600 2.3 2.370456
800 1.7 1.799381

Figure 4: Field of view for lenses with different focal lengths.

Show the code chunk
## Look at a plot
ggplot(fov, aes(x=focal_length, y=horiz_fov)) +
  geom_point() +
  geom_line(aes(x=focal_length, y=fit_fov)) +
  labs(x = "focal length (mm)",
       y = "Field of View (degrees)")

## Here is a function that can be used in some code
field <- function(focal_len){
  ## Field of View approximation
  fit <- 1088.15264*focal_len^-0.95814
  return(fit)
} ## end function field

## Test the function with a lens focal length
focal_length <- 100
field <- field(focal_length)

test_result <- paste0("A lens with a focal length of ", focal_length, 
                      " (mm) has an approximate field of view of ",
                      round(field, digits=1)," (degrees).")
cat(test_result)
A lens with a focal length of 100 (mm) has an approximate field of view of 13.2 (degrees).

Figure 5: Field of view for lenses with different focal lengths.