rtidyverseterra

Calculate average distance to coastline in R


I'm using R and the terra package for spatial data operations.

For a number of points on Earth, I want to calculate the average distance to a coastline. So far, I have managed to calculate the minimum distance to a coastline:

# libraries
library(tidyverse)
library(sf)
library(terra)
library(rnaturalearth)


# generate a pixel raster at 200 km resolution
rasterPx_200km     <- terra::rast(resolution = 200000,              # 200km x 200 km resolution
                                  xmin = -17367530, xmax=17432470,  # custom extent (without Antarctica)
                                  ymin=-6045743, ymax=7154257,
                                  crs = "EPSG:6933")                # metric WGS84 


# convert into polygons
rasterPoly_200km      <- terra::as.polygons(rasterPx_200km, crs = "epsg:6933")  # raster to poly
rasterPoly_200km$rid  <- 1:nrow(rasterPoly_200km)                               # add raster id



# get center points
centroidPoints          <- terra::centroids(rasterPoly_200km)   # SpatVector      
centroidPoints_df       <- centroidPoints %>% sf::st_as_sf()    # data frame

# get coastline from Natural Earth
coastline         <- rnaturalearth::ne_coastline(scale = 110)                      
coastline_union   <- sf::st_union(coastline)                        # union into MultiLineString
coastline_proj    <- sf::st_transform(coastline_union, crs = 6933)  # same projection as centroidPoints


# calculate shortest distance from coast
minDistanceCoast    <- st_distance(coastline_proj, # coastline as one multiline element
                                   centroidPoints_df, # points
                                   unit="m", 
                                   pairwise = TRUE)   # vector (columns contain distances)

# data frame with distance to coast
minDistanceCoast_df <- minDistanceCoast  %>%
  t()                                    %>%   # transpose to set up columns vs. rows for data frame
  as.data.frame()                        %>%   # make data frame
  rename("minDistanceCoast" = 1)               # rename first column


units(minDistanceCoast_df$minDistanceCoast) <- "km"  # set to kilometres (automatically makes a conversion)


# full data frame with raster IDs
minDistanceCoast_full <- centroidPoints_df %>%
  cbind(minDistanceCoast_df)

The resulting data frame minDistanceCoast_full contains the id of each point (called rid) and the minimum distance from the coastline (without differentiating for land and water pixels). How can I adapt my code to calculate the average distance for each point in centroidPoints_df to the coast line object coastline_proj? The result should be a data frame containing the columns rid and avgDistanceCoast.

My idea would be to generate rays from the points to a number of different directions and measure the distance whenever they touch a coastline; then averaging the distances. It would be good if the number of rays to calculate the average could be set in a function. I'd like to start with 8 distances in the cardinal and intercardinal directions (N, NE, E, ...).


Solution

  • This is a cleaned-up answer that shows how to get the average distance to the coast from a point on land, when traveling along paths of constant bearing.

    First a function to get the paths

    get_dirs <- function(n) {
        dirs <- vect(cbind(0,0), crs="local") |> 
                buffer(360, quads=round(n/4)) |> crds() |> unique()
        dirs <- cbind(0, dirs[,1], 0, dirs[,2]) |> as.lines()
        values(dirs) <- data.frame(ID=1:nrow(dirs))
        crs(dirs) <- "+proj=longlat"
        dirs
    }
    

    And a function to get the average distance

    get_dist <- function(land, directions, lonlat) {
        v <- vect(lonlat, crs="lonlat")
        i <- which(relate(v, land, "intersects"))
        if (length(i) == 0) return(NA)
        drs <- shift(directions, lonlat[1], lonlat[2])
        edr <- crop(drs, land[i]) |> disagg()
    #   i <- which(relate(edr, buffer(v, 10), "intersects"))
        i <- which(round(distance(edr, v), -1) == 0)
        e <- densify(edr[i], 0.1, flat=TRUE)
        d <- perim(e) / 1000
        mean(d)
    }
    

    Example data, here using Africa. Remove the country boundaries, and disaggregate to separate islands/continents

    library(terra)
    land <- rnaturalearth::ne_countries(scale = 110, ret="sv")
    africa <- land[land$continent == "Africa", ] |> aggregate() |> disagg()
    # example points
    pnts <- cbind(c(-10, 20, 20), c(10, -20, 20))
    

    And a solution using 360 directions

    dirs <- get_dirs(360)
    sapply(1:nrow(pnts), \(i) get_dist(africa, dirs, pnts[i,,drop=F]))
    # [1] 1804.092 2176.476 2766.308
    

    Illustrated for the first point:

    v <- vect(pnts[1,,drop=FALSE], crs="lonlat")
    drs <- shift(dirs, pnts[1,1], pnts[1,2])
    edr <- crop(drs, africa) |> disagg()
    i <- which(round(distance(edr, v), -1) == 0)
    plot(africa, col="light gray", border=NA, background="azure")
    lines(edr, col="blue"); lines(edr[i], col="red")
    points(edr[i], cex=.25); points(v)
    

    enter image description here