--- title: "viewscape" author: "Xiaohao Yang" date: "2024-06-06" vignette: > %\VignetteIndexEntry{viewscape} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- # Viewscape This vignette provides a basic overview of the functions in R package `viewscape`. The basic viewshed analysis can be accessed through calling the `compute_viewshed`. The two needed objects to compute the viewshed are a digital surface model (DSM) and a viewpoint. ## 1. Compute viewshed ```{r eval=FALSE} library(viewscape) ``` ### 1.1 Compute single viewshed ```{r eval=FALSE} #Load in DSM test_dsm <- terra::rast(system.file("test_dsm.tif", package ="viewscape")) #Load in the viewpoints # Load points (.shp file) test_viewpoints <- sf::read_sf(system.file("test_viewpoints.shp", package = "viewscape")) test_viewpoint <- test_viewpoints[2,] #Compute viewshed output <- viewscape::compute_viewshed(dsm = test_dsm, viewpoints = test_viewpoint, offset_viewpoint = 3, r = 800, method = 'plane') ``` ```{r eval=FALSE} # overlap viewshed on DSM output_r <- viewscape::visualize_viewshed(output, outputtype = 'raster') terra::plot(test_dsm, axes=FALSE, box=FALSE, legend = FALSE) terra::plot(output_r, add=TRUE, col = "red", axes=FALSE, box=FALSE, legend = FALSE) terra::plot(test_viewpoint, add = TRUE, col = "blue", axes=FALSE, box=FALSE, legend = FALSE) ``` ### 1.2 Subset viewshed using by specifying the field of view Angles are measured clockwise from north: 0 = north, 90 = east, −90 = west, ±180 = south. ```{r eval=FALSE} # north-facing 120° arc (−60° to 60°) sector <- viewscape::fov_mask(output, c(-60, 60)) terra::plot(test_dsm, axes=FALSE, box=FALSE, legend = FALSE) terra::plot(viewscape::visualize_viewshed(sector, outputtype = 'raster'), axes=FALSE, box=FALSE, legend = FALSE, add = TRUE, col = "red") terra::plot(test_viewpoint, add = TRUE, col = "blue", axes=FALSE, box=FALSE, legend = FALSE) ``` ### 1.3 Compute visual magnitude Visual Magnitude (VM) quantifies how much of the observer's visual field each visible cell occupies. It combines three factors: the surface area of the cell (resolution²), the angle between the cell's surface normal and the line of sight, and the squared distance from the observer. Cells that are large, face the observer directly, and are nearby have the highest visual magnitude. The method follows Chamberlain & Meitner (2013). ```{r eval=FALSE} vm <- viewscape::visual_magnitude(output, test_dsm) ``` Plot the raw VM raster. Higher values (warmer colours) indicate cells that dominate the visual field: ```{r eval=FALSE} terra::plot(test_dsm, axes = FALSE, box = FALSE, legend = FALSE, main = "Visual magnitude over DSM", col = gray.colors(256, start = 0.1, end = 0.9)) terra::plot(sqrt(vm), add=TRUE, alpha=0.8, axes=FALSE, box=FALSE) terra::plot(test_viewpoint, add = TRUE, col = "red", pch = 10, axes = FALSE, box = FALSE, legend = FALSE) ``` Summarise the distribution of VM values across the viewshed: ```{r eval=FALSE} vm_vals <- terra::values(vm, na.rm = TRUE) summary(vm_vals) # Total visual magnitude (sums to ~1 for a full hemisphere in theory) sum(vm_vals) # Identify the top 10% most visually dominant cells threshold <- quantile(vm_vals, 0.90) vm_top10 <- terra::classify(vm, cbind(0, threshold, NA)) # mask low values terra::plot(test_dsm, axes = FALSE, box = FALSE, legend = FALSE, main = "Top 10% most visually dominant cells", col = gray.colors(256, start = 0.1, end = 0.9)) terra::plot(vm_top10, add = TRUE, col = "firebrick", axes = FALSE, box = FALSE, legend = FALSE) terra::plot(test_viewpoint, add = TRUE, col = "blue", pch = 16, axes = FALSE, box = FALSE, legend = FALSE) ``` VM can also be computed after subsetting the viewshed with `fov_mask()` to restrict the analysis to a specific field of view: ```{r eval=FALSE} # Visual magnitude within a 120° north-east arc (0° to 120°) sector <- viewscape::fov_mask(output, c(0, 120)) vm_sector <- viewscape::visual_magnitude(sector, test_dsm) terra::plot(vm_sector, axes = FALSE, box = FALSE, main = "Visual magnitude — 120° north-east arc") terra::plot(test_viewpoint, add = TRUE, col = "blue", pch = 16, axes = FALSE, box = FALSE, legend = FALSE) ``` ### 1.4 Compute the viewshed for multiple viewpoints ```{r eval=FALSE} # Compute viewsheds output <- viewscape::compute_viewshed(dsm = test_dsm, viewpoints = test_viewpoints, offset_viewpoint = 10, parallel = TRUE, workers = 1) ``` ```{r eval = FALSE} # Use plot all viewsheds on DSM par(mfrow=c(3,3)) for(i in 1:length(output)) { each <- output[[i]] raster_data <- viewscape::visualize_viewshed(each, outputtype="raster") terra::plot(test_dsm, axes=FALSE, box=FALSE, legend = FALSE) terra::plot(raster_data, add=TRUE, col = "red", axes=FALSE, box=FALSE, legend = FALSE) } ``` ### 1.5 Compute an intervisibility network `intervis_network()` tests every pair of viewpoints for mutual line of sight and returns the results either as an adjacency matrix or as a spatial network of lines. #### Adjacency matrix Each entry `[i, j]` is `1` if viewpoint `i` can see viewpoint `j`, `0` if not, and `NA` on the diagonal. Because observer heights can differ between viewpoints, visibility is not always symmetric: `[i, j]` may differ from `[j, i]`. ```{r eval=FALSE} # Binary N x N adjacency matrix mat <- viewscape::intervis_network( viewpoints = test_viewpoints, dsm = test_dsm, offset_viewpoint = 10, r = 800 ) mat ``` #### Network as spatial lines Setting `output = "lines"` returns an `sf` data frame of `LINESTRING` geometries — one per visible pair. The `mutual` column is `TRUE` when both viewpoints can see each other. ```{r eval=FALSE} net <- viewscape::intervis_network( viewpoints = test_viewpoints, dsm = test_dsm, offset_viewpoint = 10, r = 800, output = "lines" ) net ``` Plot the network over the DSM to map which viewpoints are intervisible: ```{r eval=FALSE} terra::plot(test_dsm, axes = FALSE, box = FALSE, legend = FALSE, main = "Intervisibility network") # All visible links in grey terra::plot(net["geometry"], add = TRUE, col = "grey60", lwd = 1) # Mutually visible links in blue terra::plot(net[net$mutual, "geometry"], add = TRUE, col = "#2171b5", lwd = 2) # Viewpoints terra::plot(test_viewpoints, add = TRUE, col = "red", pch = 16, cex = 1.2) ``` #### Per-viewpoint observer heights Pass a vector of heights to `offset_viewpoint` to model different observer elevations at each point — for example, observers on rooftops of varying height: ```{r eval=FALSE} n_vp <- nrow(sf::st_coordinates(test_viewpoints)) # cycle through a set of representative heights, one per viewpoint heights <- rep(c(1.7, 6, 12), length.out = n_vp) mat_heights <- viewscape::intervis_network( viewpoints = test_viewpoints, dsm = test_dsm, offset_viewpoint = heights, r = 1600 ) mat_heights ``` ## 2. Calculate viewscape metrics ### 2.1 Calculate the metrics of viewshed The function of view depth analysis can calculate two different metrics: the furthest distance and standard deviation of distances. To calculate view depth, there are two needed objects: the DSM that was used to get viewshed and result from viewshed analysis. The function of extent analysis can calculate the total area of viewshed and needs the DSM that was used to get viewshed and result from viewshed analysis. The following function can calculate the area of ground surface and standard deviation of elevations within a viewshed. The function needs a DSM and a DEM/DTM to calculate the metrics. ```{r eval=FALSE} #Load in DSM test_dsm <- terra::rast(system.file("test_dsm.tif", package ="viewscape")) # Load DTM test_dtm <- terra::rast(system.file("test_dtm.tif", package ="viewscape")) # Load canopy raster test_canopy <- terra::rast(system.file("test_canopy.tif", package ="viewscape")) # Load building footprints raster test_building <- terra::rast(system.file("test_building.tif", package ="viewscape")) ``` ```{r eval=FALSE} # calculate metrics given the viewshed, canopy, and building footprints test_metrics <- viewscape::calculate_viewmetrics(output[[1]], test_dsm, test_dtm, list(test_canopy, test_building)) test_metrics ``` ### 2.2 Calculate land use/cover diversity calculate_diversity() calculates the proportion of each type of land use/ cover within a viewshed to get the Shannon Diversity Index. ```{r eval=FALSE} # load landuse raster test_landuse <- terra::rast(system.file("test_landuse.tif", package ="viewscape")) ``` ```{r eval=FALSE} # the Shannon Diversity Index (SDI) test_diversity <- viewscape::calculate_diversity(output[[1]], test_landuse, proportion = TRUE) # SDI and The proportion of each type of land use test_diversity ``` ### 2.3 calculate a single feature calculate_feature is to calculate the proportion of a feature (including trees, buildings, parking, and roads) within the viewshed. This function can be applied to ```{r eval=FALSE} # calculate the percentage of canopy coverage test_canopy_proportion <- viewscape::calculate_feature(viewshed = output[[1]], feature = test_canopy, type = 2, exclude_value=0) test_canopy_proportion ``` ## 3. Panoramic view `pano_view()` generates a 360° equirectangular (or cylindrical) panorama from a viewpoint using raycasting through the DSM. The output type depends on the inputs: | `method` | `semantic` | result | |---|---|---| | `"equirectangular"` (default) | `NULL` | 3-band RGB colour panorama (auto-downloads ESRI WorldImagery via **greenSD**) | | `"equirectangular"` | land-cover raster | 2-layer: depth (m) + land-cover codes | | `"cylindrical"` | `NULL` | single-layer distance panorama | | `"cylindrical"` | any raster | 2-layer: depth + semantic class | ### 3.1 Colour panorama (equirectangular, default) When no `semantic` layer is supplied, `pano_view()` automatically fetches ESRI WorldImagery tiles for the DSM area via `greenSD::get_tile_green()` and projects the satellite RGB colours onto the panoramic rays. Sky pixels are filled with a configurable sky-blue colour. ```{r eval=FALSE} # Requires the greenSD package: # devtools::install_github("billbillbilly/greenSD") # Default: equirectangular + ESRI WorldImagery → 3-band RGB panorama pano_rgb <- viewscape::pano_view( dsm = test_dsm, vpt = test_viewpoint, h = 3, # observer height above ground (m) plot = TRUE ) # pano_rgb is a 3-band SpatRaster (R, G, B, 0–255) # Use plotRGB() to display it at any time: terra::plotRGB(pano_rgb, r = 1, g = 2, b = 3) ``` Change the sky colour or the panorama dimensions: ```{r eval=FALSE} pano_custom <- viewscape::pano_view( dsm = test_dsm, vpt = test_viewpoint, h = 6, pano_dim = c(512, 1024), # taller panorama for more vertical detail sky_color = c(200, 220, 255), # pale-blue sky heading = 90, # face east plot = TRUE ) ``` ### 3.2 Land-cover / land-use panorama (equirectangular + semantic) When a `semantic` raster is supplied, `pano_view()` encodes the land-cover or land-use code at each ray-hit cell instead of the satellite colour. The result has two layers: a depth layer (distance in metres to the first surface hit) and a semantic layer (the code from the input raster). ```{r eval=FALSE} test_landuse <- terra::rast(system.file("test_landuse.tif", package = "viewscape")) pano_lc <- viewscape::pano_view( dsm = test_dsm, vpt = test_viewpoint, h = 5, semantic = test_landuse, # land-use raster with class codes plot = TRUE # plots depth (left) and land-use codes (right) ) # Access individual layers depth_pano <- pano_lc[["equirectangular_depth"]] lc_pano <- pano_lc[["equirectangular_semantic"]] terra::plot(lc_pano, main = "Land use codes in panoramic view") ``` Binary rasters (values 0/1 — e.g. building footprints, canopy masks) are detected automatically and treated as vertical extrusions: the semantic value fills the full column of voxels up to the DSM surface, matching the voxel-style projection of Lu et al. (2020). ```{r eval=FALSE} test_building <- terra::rast(system.file("test_building.tif", package = "viewscape")) pano_bld <- viewscape::pano_view( dsm = test_dsm, vpt = test_viewpoint, h = 6, semantic = test_building, # binary building-footprint mask plot = TRUE ) ``` ```{r eval=FALSE} test_building <- terra::rast(system.file("test_building.tif", package = "viewscape")) pano_bld <- viewscape::pano_view( dsm = test_dsm, vpt = test_viewpoint, h = 6, semantic = test_canopy, # binary tree canopy mask plot = TRUE ) ```