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

  • Something like this could work.

    Example data

    library(terra)
    coast <- rnaturalearth::ne_coastline(scale = 110, ret="sv") |> aggregate()
    # the below is to make sure it also works in the Pacific Ocean
    E <- crop(coast, ext(90, 180, -90, 90))
    W <- crop(coast, ext(-180, 0, -90, 90))
    coast <- rbind(shift(E, -360), coast, shift(W, 360)) |> aggregate()
    
    # your points
    pnts <- cbind(c(90,-30, 20), c(-20,20,10))
    vpnts <- vect(pnts, crs=coast)
    

    Solution

    directs <- vect(c("LINESTRING (0 180, 0 -180)", # EW 
                      "LINESTRING (-180 -180, 180 180)", # SW-NE
                      "LINESTRING (-180 180, 180 -180)", # NW-SE
                      "LINESTRING (-360 0, 360 0)"), # NS
                           crs=coast)
    
    mdist <- rep(NA, nrow(pnts))
    
    i <- 1
    #for (i in 1:nrow(pnts)) {
    drs <- shift(directs, pnts[i,1], pnts[i,2])
    edr <- erase(drs, coast) |> disagg()
    j <- which(relate(edr, buffer(vpnts[i], 100), "intersects"))
    d <- distance(as.points(edr[j]), vpnts[i], unit="km")
    mdist[i] <- mean(d)
    #}
    
    d
    [1,] 4671.684
    [2,] 5251.762
    [3,] 6366.009
    [4,] 2238.266
    [5,] 5722.547
    [6,] 6037.589
    [7,] 4306.038
    [8,] 3041.791
    
    mdist[i]
    [1] 4704.461
    

    Illustrated below. Note that the red lines are not the shortest paths (except in the NS direction).

    countries <- rnaturalearth::ne_countries(scale = 110, ret="sv") |> aggregate()
    plot(countries, col="light gray", background="azure", border="gray", buffer=FALSE)
    lines(drs)
    lines(edr[j], col="red", lwd=3)
    points(vpnts[i], pch=1)
    points(edr[j], col="blue")
    
    # add shortest paths
    p <- crds(as.points(edr[j]))
    p <- terra::as.lines(cbind(pnts[i,1], p[,1], pnts[i,2], p[,2]), crs=crs(edr)) |> densify(100000)
    lines(p, lty=2)
    

    enter image description here

    For the third point (on land):

    enter image description here

    A function to create directions could look like this

    directions <- function(d) {
        d <- d * pi/180
        sd <- rep(cos(d), each=2) * c(-360, 360)
        cd <- rep(sin(d), each=2) * c(-180, 180)
        sdcd <- cbind(rep(1:length(d), each=2), sd, cd)
        vect(sdcd, "lines", crs="lonlat")
    }
    
    d <- directions(seq(0, 150, 30))
    plot(countries, asp=2)
    lines(d, col="red")
    

    enter image description here


    The following gets the last intersections (as requested in a comment).

    library(terra)
    conts <- rnaturalearth::ne_countries(scale = 110, ret="sv")
    af <- conts[conts$continent == "Africa", ]
    set.seed(1)
    af <- sample(af, 25)
    pnt <- cbind(20, 10)
    
    # have separate segments for N and S etc.
    directs <- vect(c("LINESTRING (0 180, 0 0)" , "LINESTRING(0 0, 0 -180)", # E and W 
                      "LINESTRING (-180 -180, 0 0)","LINESTRING(0 0, 180 180)", # SW and NE
                      "LINESTRING (-180 180, 0 0)", "LINESTRING(0 0, 180 -180)", # NW-SE
                      "LINESTRING (-360 0, 0 0)", "LINESTRING(0 0, 360 0)"), # N and S
                         crs=af)
    values(directs) <- data.frame(ID=1:8)
    
    z <- shift(directs, pnt[,1], pnt[,2]) |> crop(af)
    g <- geom(z)
    dist <- distance(g[, c("x", "y")], pnt, lonlat=T)
    d <- data.frame(geom=g[,1], g[, c("x", "y")], dist=dist)
    m <- merge(aggregate(dist ~ geom, max, data = d), d)
    m
    #  geom      dist          x          y
    #1    1 1264026.4  20.000000  21.422728
    #2    2 3522543.5  20.000000 -21.845463
    #3    3  333993.1  17.858601   7.858601
    #4    4 1845315.3  32.000000  22.000000
    #5    5 3863310.6  -5.758871  35.758871
    #6    6 3105606.6  39.841971  -9.841971
    #7    7 3758767.3 -14.299010  10.000000
    #8    8 3371388.6  50.761263  10.000000
    
    plot(af, col="light gray", border="gray", lwd=2); lines(z, col="blue", lwd=2); points(z)
    points(m[, c("x", "y")], col="red", pch=20, cex=2)
    

    enter image description here