Analyze Building Footprints

This vignette shows how to compute building-level metrics from a building footprint layer with height attributes. Most functions return the same sf object with additional metric columns, so the usual workflow is to pipe or reassign the result step by step.

library(gloBFPr)
library(sf)
library(dplyr)

Example data

The package includes a small sf example dataset with building footprints, unique IDs, and building heights.

data(globfp_example)

buildings <- globfp_example
buildings <- buildings[seq_len(min(10, nrow(buildings))), ]

names(buildings)
## [1] "Height"   "geometry" "id"

For your own area of interest, first retrieve building footprints with search_3dglobdf(), then pass the returned polygon layer to the metric functions.

buildings <- search_3dglobdf(
  bbox = c(-83.065644, 42.333792, -83.045217, 42.346988),
  out_type = "poly",
  quiet = TRUE
)

1 Morphological Metrics

get_morphology() computes geometric and shape metrics such as footprint area, perimeter, volume, compactness, convexity, elongation, and pairwise distance.

morphology <- get_morphology(buildings, quiet = TRUE)

morphology |>
  st_drop_geometry() |>
  select(id, Height, g_area, pmeter, vol, rec, cnv, elo_z) |>
  head()

You can also request only a subset of metrics when you do not need the full set.

basic_shape <- get_morphology(
  buildings,
  metrics = c("g_area", "pmeter", "vol", "rec"),
  quiet = TRUE
)

2 Neighbor Metrics

get_neighbors() estimates the number of neighboring buildings and centroid distance summaries using a fixed search radius and Voronoi adjacency.

neighbors <- get_neighbors(
  morphology,
  radius = 500,
  quiet = TRUE
)

neighbors |>
  st_drop_geometry() |>
  select(id, n_count, m_ndist, min_ndist, max_ndist, sd_ndist) |>
  head()

Use a smaller radius for dense local context or a larger radius for broader neighborhood context.

neighbors_100m <- get_neighbors(buildings, radius = 100, quiet = TRUE)

3 Distance to Nearest Greenspace

get_dng() computes the distance from each building centroid to the nearest qualifying green patch within a buffer. The source can be metachm, esri, or sentinel2.

By default the distance is measured in a straight line. Every result also carries a dng_method column recording how the value was obtained, which matters once network routing is switched on below.

dng <- get_dng(
  neighbors,
  datasource = "metachm",
  radius = 800,
  min_tree_height = 2,
  min_area = 500,
  unit = "m2",
  quiet = TRUE
)

dng |>
  st_drop_geometry() |>
  select(id, dng, dng_method) |>
  head()

For a 2D greenery mask source, use esri or sentinel2.

dng_esri <- get_dng(
  buildings,
  datasource = "esri",
  zoom = 17,
  radius = 800,
  min_area = 500,
  unit = "m2",
  quiet = TRUE
)

Measuring Along Real Streets

Straight-line distance ignores rivers, rail corridors, walls, and the simple fact that people walk on streets. Set network = "osm" to measure along real road and path centre lines instead. get_dng() downloads a walkable OpenStreetMap network for the study extent (motorways, trunk roads, and ways tagged foot=no are excluded), builds a routing graph, and reports

centroid to network  +  shortest path along network  +  network to green pixel
dng_net <- get_dng(
  neighbors,
  datasource = "metachm",
  radius = 800,
  min_area = 500,
  unit = "m2",
  network = "osm",
  quiet = TRUE
)

dng_net |>
  st_drop_geometry() |>
  select(id, dng, dng_method) |>
  head()

The network is downloaded once for the whole study area rather than once per building, so routing adds a single Overpass request regardless of how many buildings you pass in. If Overpass is unreachable or returns nothing, get_dng() warns and falls back to straight-line distance rather than failing.

Supply your own lines when you need a specific network — a cleaned municipal sidewalk layer, a snapshot pinned to a particular date, or the same network you passed to generate_block(). Any sf line layer works, and it is reprojected for you.

# roads is an sf LINESTRING layer you already have
dng_custom <- get_dng(
  neighbors,
  datasource = "metachm",
  radius = 800,
  min_area = 500,
  unit = "m2",
  network = roads,
  quiet = TRUE
)

dng_method tells you which measure each row actually used. A building falls back to "euclidean" when no path connects it to any green patch — typically an isolated network fragment, or a building whose buffer extends past the edge of the downloaded network. Check it before interpreting results, and compare the two measures to see where the street layout imposes a real detour:

table(dng_net$dng_method)

comparison <- data.frame(
  id        = dng$id,
  euclidean = dng$dng,
  network   = dng_net$dng
)
comparison$detour_ratio <- comparison$network / comparison$euclidean

# Ratios near 1 mean the green space is straight ahead; large ratios flag
# buildings cut off by barriers, dead ends, or missing crossings.
head(comparison[order(-comparison$detour_ratio), ])

A few things to keep in mind. Distances are computed in the projected UTM CRS of your study area, so they are in metres. Green space is compared pixel by pixel, so a higher zoom gives a finer target set at the cost of runtime. And buildings connect to the graph at its vertices, so the network is densified to about 20 m spacing before routing — accurate enough for walking distances without inflating the graph.

4 Building Green View Index

get_bgvi() estimates Building Green View Index from building viewpoints. The DSM can include metaCHM canopy height, while the visible-green feature layer can come from height-filtered canopy, 2D map-tile greenspace, or the union of both. An OpenTopography API key is required for the DEM. This can be computationally heavy; start with a small subset of buildings.

bgvi <- get_bgvi(
  buildings[1:10, ],
  datasource_canopy_height = "metachm",
  datasource_greenspace = "esri",
  min_tree_height = 2,
  radius = 800,
  floor = FALSE,
  workers = 1,
  key = Sys.getenv('OPENTOPO_API'),
  quiet = TRUE
)

bgvi |>
  st_drop_geometry() |>
  select(id, mean_gvi, bottom_gvi, top_gvi) |>
  head()

To compute BGVI with a DSM built only from buildings and DEM, set datasource_canopy_height = NULL and provide a 2D greenspace source.

bgvi_2d_green <- get_bgvi(
  buildings[1:3, ],
  datasource_canopy_height = NULL,
  datasource_greenspace = "esri",
  radius = 800,
  short_building_threshold = 6,
  workers = 1,
  key = Sys.getenv('OPENTOPO_API'),
  quiet = TRUE
)

To compute directional BGVI, provide one or more directions and a field of view in degrees. The function reuses each computed viewshed and filters visible greenery by direction.

bgvi_directional <- get_bgvi(
  buildings[1:3, ],
  datasource_canopy_height = "metachm",
  datasource_greenspace = "esri",
  radius = 800,
  directions = c("southwest", "south", "southeast"),
  field_of_view = 45,
  workers = 1,
  key = Sys.getenv('OPENTOPO_API'),
  quiet = TRUE
)

bgvi_directional |>
  st_drop_geometry() |>
  select(id, mean_gvi_south, gvi_bottom_south, gvi_top_south) |>
  head()

Use plot_bgvi_viewshed() when you want to inspect the viewshed behind one building’s BGVI value. It uses the same DSM and green-feature preparation as get_bgvi(), but computes a single viewpoint and returns the diagnostic rasters invisibly when plot = TRUE.

top_direction_view <- plot_bgvi_viewshed(
  buildings,
  building = 195,
  level = "top",
  orientation = "north",
  field_of_view = 90,
  datasource_canopy_height = "metachm",
  datasource_greenspace = "esri",
  radius = 500,
  key = Sys.getenv('OPENTOPO_API'),
  quiet = TRUE
)

You can also inspect a specific floor or supply an observer height directly.

fifth_floor_view <- plot_bgvi_viewshed(
  buildings,
  building = 1,
  floor = 5,
  orientation = 135,
  field_of_view = 90,
  datasource_canopy_height = "metachm",
  datasource_greenspace = "esri",
  radius = 500,
  key = Sys.getenv('OPENTOPO_API'),
  quiet = TRUE
)

fifth_floor_view$gvi
fifth_floor_view$visible_green

To compute GVI by floor, set floor = TRUE. Use floor_step to sample every n floors and reduce runtime. The top estimated floor is always included.

bgvi_by_floor <- get_bgvi(
  buildings[1:3, ],
  datasource_canopy_height = "metachm",
  radius = 800,
  floor = TRUE,
  floor_step = 3,
  workers = 2,
  key = Sys.getenv('OPENTOPO_API'),
  quiet = TRUE
)

bgvi_by_floor |>
  st_drop_geometry() |>
  select(id, estimated_floors, mean_gvi, bottom_gvi, top_gvi, min_gvi, max_gvi, sd_gvi) |>
  head()

5 A Complete Metric Pipeline

For a typical analysis, build the result incrementally. Keep quiet = TRUE for batch workflows, or set it to FALSE when you want messages and progress bars.

result <- buildings |>
  get_morphology(quiet = TRUE) |>
  get_neighbors(radius = 500, quiet = TRUE) |>
  get_dng(
    datasource = "metachm",
    radius = 800,
    min_area = 500,
    unit = "m2",
    network = "osm",
    quiet = TRUE
  )

metric_table <- result |>
  st_drop_geometry() |>
  select(
    id, Height, g_area, vol, rec,
    n_count, m_ndist, min_ndist, max_ndist, sd_ndist,
    dng, dng_method
  )

head(metric_table)