Plot an ellipse between two geographical foci in R - r

I am trying to find out the geographical area that is equidistant from two points, and to plot this as an ellipse.
I can produce plots for one point easily using st_buffer, and can find numerous R functions that will plot ellipse from a known centroid if I define the axis, but have not been able to find one that will plot an ellipse given two known foci and a defined distance.
The similar question here gets some way towards an answer, but is not readily applicable to geographic situations - Draw an ellipse based on its foci
My code is pretty simple at the moment, and given each coordinate with a 100km radius. However, I would like to find out all the positions that would be reachable by a 200km (or other defined distance) trip between both sites.
library(tidyverse)
library(sf)
#Give Coordinates
citylocations <- tibble::tribble(
~city, ~lon, ~lat,
"London", -0.1276, 51.5072,
"Birmingham", -1.8904, 52.4862,
)
citydflocations <- as.data.frame(citylocations)
#Convert to SF
citysflocations <- sf::st_as_sf(citydflocations, coords = c("lon","lat" ), crs = 4326)
#Convert location file to National Grid Planar
cityBNGsflocations <- citysflocations %>%
st_transform(citysflocations, crs = 27700)
#Produce circles with 100km buffer
dat_circles <- st_buffer(cityBNGsflocations, dist = 100000)
join_circles <- st_union(dat_circles) %>%
st_transform(4326)
plot(join_circles, col = 'lightblue')```

The function below should create buffers of varying distances for each of the two points it is given, finds the intersection the two buffers, unions the intersections, and finally returns a convex hull of those intersections. The output should be a near approximation of an ellipse with the two points as foci.
The straight-line(s) distance from one city to any edge of the polygon and then to the other city should equal the distance given in the function (200,000m in the example below).
It works on the data provided, but is fragile as there's no error checking or warning suppression. Make sure the dist argument is greater than the distance between the two points, and that the points have a crs that can use meters as a distance. (lat/lon might not work)
The example below only uses 20 points for the 'ellipse', but changing the function should be relatively straightforward.
library(sf)
#> Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 6.3.1; sf_use_s2() is TRUE
library(tidyverse)
#Give Coordinates
citylocations <- tibble::tribble(
~city, ~lon, ~lat,
"London", -0.1276, 51.5072,
"Birmingham", -1.8904, 52.4862,
)
citydflocations <- as.data.frame(citylocations)
#Convert to SF
citysflocations <- sf::st_as_sf(citydflocations, coords = c("lon","lat" ), crs = 4326)
#Convert location file to National Grid Planar
cityBNGsflocations <- citysflocations %>%
st_transform(citysflocations, crs = 27700)
#Produce circles with 100km buffer
dat_circles <- st_buffer(cityBNGsflocations, dist = 100000)
join_circles <- st_union(dat_circles) %>%
st_transform(4326)
#plot(join_circles, col = 'lightblue')
### the ellipse function using 20 buffers ####
ellipse_fn <- function(x_sf, y_sf, distance){
#set distance argument to meters, get sequence of distances for buffers
distance = units::set_units(distance, 'm')
dists_1 <- seq(units::set_units(0, 'm'), distance, length.out = 22)
# create empty sf object to place for loop objects in
# purrr would probably be better here
nrows <- 20
df <- st_sf(city = rep(NA, nrows), city.1 = rep(NA, nrows), geometry = st_sfc(lapply(1:nrows, function(x) st_geometrycollection())))
intersections <- for(i in 2:21){
buff_1 <- st_buffer(cityBNGsflocations[1,], dist = dists_1[i])
buff_2 <- st_buffer(cityBNGsflocations[2,], dist = distance - dists_1[i])
intersection <- st_intersection(buff_1, buff_2)
df[i-1,] <- intersection
}
df %>%
st_set_crs(st_crs(x_sf)) %>%
st_union() %>%
st_convex_hull()
}
### end ellipse function ###
# Using the ellipse function with 2 points & 200000m distance
ellipse_sf <- ellipse_fn(cityBNGsflocations[1,], cityBNGsflocations[2,], dist = 200000)
# You'll get lots of warnings here about spatial constance...
ggplot() +
geom_sf(data = ellipse_sf, fill = 'black', alpha = .2) +
geom_sf(data = cityBNGsflocations, color = 'red')
Created on 2022-06-03 by the reprex package (v2.0.1)
mapview plot of the cities & 'ellipse' on a map:

Related

create evenly spaced polylines over counties using R

I would like to create evenly spaced polylines going North to South with 50 mile spacing between each line and 10 miles long. Not sure if this is possible using sf package. In the example below, I would like to have the lines filling the counties across the state of Washington.
library(tigris)
library(leaflet)
states <- states(cb = TRUE)
counties<-counties(cb=TRUE)
counties<- counties%>%filter(STATEFP==53)
states<- states%>%filter(NAME=="Washington")
leaflet(states) %>%
addProviderTiles("CartoDB.Positron") %>%
addPolygons(fillColor = "white",
color = "black",
weight = 0.5) %>%
addPolygons(data=counties,color='red',fillColor = 'white')%>%
setView(-120.5, 47.3, zoom=8)
I've updated to include an image of what I'd like to do below.
You can create a multilinestring sf object from scratch by specifying coordinates.
You can get these coordinates from the extent (bounding box) of Washington, but you may also be interested in knowing how to create a grid, which I will demonstrate below because it may be helpful.
Copy and paste this reproducible example:
library(tidyverse)
library(tigris)
library(leaflet)
library(sf)
library(raster)
states <- states(cb = TRUE)
# subset for WA and transform to a meter-based CRS
states <- states %>%
filter(NAME == "Washington") %>%
st_transform(crs = 3857) # Mercator
# fifty miles in meters
fm <- 80467.2
# subset for Washington
states_sp <- as(states, "Spatial")
# create a grid, convert it to polygons to plot
grid <- raster(extent(states_sp),
resolution = c(fm, fm),
crs = proj4string(states_sp))
grid <- rasterToPolygons(grid)
plot(states_sp)
plot(grid, add = TRUE)
# find the top y coordinate and calculate 50 mile intervals moving south
ty <- extent(grid)[4] # y coordinate along northern WA edge
ty <- ty - (fm * 0:7) # y coordinates moving south at 10 mile intervals
# create a list of sf linestring objects
l <- vector("list", length(ty))
for(i in seq_along(l)){
l[[i]] <-
st_linestring(
rbind(
c(extent(grid)[1], ty[i]),
c(extent(grid)[2], ty[i])
)
)
}
# create the multilinestring, which expects a list of linestrings
ml <- st_multilinestring(l)
plot(states_sp)
plot(as(ml, "Spatial"), add = TRUE, col = "red")
As you can see, I switch back and forth between sf and sp objects using the functions as(sf_object, "Spatial") and st_as_sf(sp_object). Use these to transform the data to your needs.

Looking for a polygon where a point is contained in R

I'm working with a dataframe containing longitude and latitude for each point. I have a shapefile containing mutually exclusive polygons. I would like to find the index of the polygon it where each point is contained. Is there a specific function that helps me achieve this? I've been trying with the sf package, but I'm open to doing it with another one. Any help is greatly appreciated.
I believe you may be looking for function sf::st_intersects() - in combination with sparse = TRUE setting it returns a list, which can be in this use case (points & a set of non-overlapping polygons) converted to a vector easily.
Consider this example, built on the North Carolina shapefile shipped with {sf}
library(sf)
# as shapefile included with sf package
shape <- st_read(system.file("shape/nc.shp", package="sf")) %>%
st_transform(4326) # WGS84 is a good default
# three semi random cities
cities <- data.frame(name = c("Raleigh", "Greensboro", "Wilmington"),
x = c(-78.633333, -79.819444, -77.912222),
y = c(35.766667, 36.08, 34.223333)) %>%
st_as_sf(coords = c("x", "y"), crs = 4326) # again, WGS84
# plot cities on full map
plot(st_geometry(shape))
plot(cities, add = T, pch = 4)
# this is your index
index_of_intersection <- st_intersects(cities, shape, sparse = T) %>%
as.numeric()
# plot on subsetted map to doublecheck
plot(st_geometry(shape[index_of_intersection, ]))
plot(cities, add = T, pch = 4)

How to get Voronoi diagram around polygons

I would like to generate a Voronoi diagram around 2D polygons. This question is somehow similar to this one here addressed for Python.
Is straightforward how this works for points, below is an example with sf::st_voronoi() function:
library(sf)
#> Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 6.3.1
p <- rbind(c(3.2,4),
c(3,4.6),
c(3.8,4.4),
c(3.5,3.8),
c(3.4,3.6),
c(3.9,4.5))
plot(p, pch = 16)
p %>% st_multipoint() %>% st_voronoi() %>% plot(col = NA, add = TRUE)
Created on 2020-05-28 by the reprex package (v0.3.0)
But when I try the same function for some generated polygons, I do not get the results I would like:
library(sf)
#> Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 6.3.1
p1 <- rbind(c(0,0), c(1,0), c(3,2), c(2,4), c(1,4), c(0,0))
p2 <- rbind(c(3,0), c(4,0), c(4,1), c(3,1), c(3,0))
pol <- st_multipolygon(list(list(p1), list(p2)))
plot(st_voronoi(pol), col = NA, lwd = 2, lty = 3)
plot(pol, col = rgb(1,0,0, alpha = 0.3), add = TRUE)
Created on 2020-05-28 by the reprex package (v0.3.0)
It seems that the Voronoi grid is based on the vertices of the polygons, which makes sense. However, I would like to get a Voronoi grid surrounding the red polygons and not intersecting them, that is, to treat a polygon as a point. Note that, getting the centroids of the polygons (e.g. with sf::st_centroid) and then generating a Voronoi grid is a path I tried, but the Voronoi grid will still intersect the polygons.
Here is my attempt. This is mostly relying on sf but I use smoothr::densify() to add vertices to straight edges of polygons (since the voronoi polygons are initially built around the polygon vertices), and I rely on a function from data.table to combine sf objects. There are probably ways to make this more efficient.
You would probably also want to simplify input polygons, although that is not needed in this test case.
The one really unresolved issue is for when two polygons share a boundary. The polygon-based voronoi should just follow that boundary, but currently does not.
library(sf)
# additionally requires:
## smoothr to densify polygons
## data.table to combine results
## poly = input sf polygons
## clip = polygon to be used as an extent for the output
## max_distance = argument for smoothr::densify, what max distance to have between vertices of a polygon. For breaking up long edges. In map units.
polyVoronoi <- function(poly, clip = NULL, max_distance = NULL) {
# add vertices to polygons to have voronoi polygons along straight edges of polygon
if (!is.null(max_distance)) {
poly <- smoothr::densify(poly, max_distance = max_distance)
}
# generate voronoi polygons for all vertices
vv <- st_voronoi(st_combine(poly))
vv <- st_collection_extract(vv, 'POLYGON')
# deal with geom validity issues
if (!all(st_is_valid(vv))) {
for (i in 1:length(vv)) {
vv[i] <- st_make_valid(vv[i])
if (!all(st_is_valid(vv[i]))) stop()
}
}
# determine which voronoi polygons intersect with input polygons
ii <- st_intersects(poly, vv)
# union/dissolve voronoi polygons that belong to the same inputs
resList <- vector('list', length(ii))
for (i in 1:length(ii)) {
xx <- vv[ii[[i]]]
xx <- st_combine(xx)
if (!all(st_is_valid(xx))) {
xx <- st_make_valid(xx)
}
resList[[i]] <- st_union(xx)
}
res <- st_as_sf(data.table::rbindlist(lapply(resList, st_as_sf)))
res <- res[1:nrow(res),]
res <- st_geometry(res)
if (!is.null(clip)) {
res <- st_intersection(res, clip)
}
return(res)
}
Example using built in dataset from sf
nc = st_read(system.file("shape/nc.shp", package="sf"))
# project to North America Albers Equal Area
nc <- st_transform(nc, crs = "+proj=aea +lat_1=20 +lat_2=60 +lat_0=40 +lon_0=-96 +x_0=0 +y_0=0 +ellps=GRS80 +datum=NAD83 +units=m +no_defs")
poly = st_geometry(nc)
# sample just a few polygons
poly <- poly[sample(1:length(poly), 12)]
# define bounds that we want for the output
e <- st_buffer(st_make_grid(poly, n = 1), 50000)
vv <- polyVoronoi(poly, clip = e, max_distance = 10000)
plot(vv, border = 'blue')
plot(poly, add = TRUE)
As you can see, there is a problem when polygons are in contact, and I haven't resolved this yet. Suggestions welcome!

Finding the radius of a circle that circumscribes a polygon

I am trying to find the best way of obtaining: the length of the longest line from the center of a polygon to its edge.
In the code below, I download the polygon data of the 75254 zip code located in Texas, USA. I then determine the location of its center with sf::st_centroid() and I plot the geometries using the tmap package.
# Useful packages
library(dplyr)
library(sf)
library(tigris)
library(tmap)
# Download polygon data
geo <- tigris::zctas(cb = TRUE, starts_with = "75254")
geo <- st_as_sf(geo)
# Determine the location of the polygon's center
geo_center <- st_centroid(geo)
# Plot geometries
tm_shape(geo) +
tm_polygons() +
tm_shape(geo_center) +
tm_dots(size = 0.1, col = "red")
Once again, is there an efficient way to determine the length of the line going from the center of the polygon all the way to the farthest point on the polygon's edge? In other words, how can I find the radius of the circle that perfectly circumscribes the polygon given that both the circle and the polygon have the same center?
Thank you very much for your help.
One point here, although I mentioned, st_bbox wouldn't work as the centroid of the bbox and the one of your shape are not the same, since the centroid is weighted. See here one approach based on the further distance to the points of the border, but you woud need to project your shape (currently is unprojected):
library(dplyr)
library(sf)
library(tigris)
library(tmap)
# Download polygon data
geo <- tigris::zctas(cb = TRUE, starts_with = "75254")
geo <- st_as_sf(geo)
st_crs(geo)
#> Coordinate Reference System:
#> EPSG: 4269
#> proj4string: "+proj=longlat +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +no_defs"
#Need to project
geo=st_transform(geo,3857)
# Determine the location of the polygon's center
geo_center <- st_centroid(geo)
#> Warning in st_centroid.sf(geo): st_centroid assumes attributes are constant over
#> geometries of x
plot(st_geometry(geo))
plot(st_geometry(geo_center), col="blue", add=TRUE)
#Cast to points
geopoints=st_cast(geo,"POINT")
#> Warning in st_cast.sf(geo, "POINT"): repeating attributes for all sub-geometries
#> for which they may not be constant
r=max(st_distance(geo_center,geopoints))
r
#> 3684.917 [m]
buffer=st_buffer(geo_center,dist=r)
plot(st_geometry(buffer), add=TRUE, border="green")
OP didn't ask for this, but in case anyone else would like to do this for multiple shapes, here's a version that builds on dieghernan's example to do that.
library(dplyr)
library(sf)
library(tigris)
library(tmap)
# Download polygon data
raw <- tigris::zctas(cb = TRUE, starts_with = "752")
geo <- raw %>%
st_as_sf() %>%
slice(1:5) %>%
st_transform(3857) %>%
arrange(GEOID10) # Sort on GEOID now so we don't have to worry about group_by resorting later
# Compute the convex hull
hull <- geo %>% st_convex_hull()
# Compute centroids
geo_center <- st_centroid(geo)
# Add centroid, then cast hull to points
hull_points <- hull %>%
mutate(centroid_geometry = geo_center$geometry) %>%
st_cast("POINT")
# Compute distance from centroid to all points in hull
hull_points$dist_to_centroid <- as.numeric(hull_points %>%
st_distance(hull_points$centroid_geometry, by_element = TRUE))
# Pick the hull point the furthest distance from the centroid
hull_max <- hull_points %>%
arrange(GEOID10) %>%
group_by(GEOID10) %>%
summarize(max_dist = max(dist_to_centroid)) %>%
ungroup()
# Draw a circle using that distance
geo_circumscribed <- geo_center %>% st_buffer(hull_max$max_dist)
# Plot the shape, the hull, the centroids, and the circumscribed circles
tm_shape(geo) +
tm_borders(col = "red") +
tm_shape(hull) +
tm_borders(col = "blue", alpha = 0.5) +
tm_shape(geo_center) +
tm_symbols(col = "red", size = 0.1) +
tm_shape(geo_circumscribed) +
tm_borders(col = "green")

Find Polygon Intercepts on a Map

I'm trying to find the Radii on this map that intercept state borders in R.
Here is my code so far. Thanks to user Gregoire Vincke for providing much of the solution.
library("maps")
library("mapproj")
library("RColorBrewer")
library("mapdata")
library("ggplot2")
library("rgeos")
library("dismo")
library("ggmap")
library("rgdal")
data("stateMapEnv") #US state map
dat <- read.csv("R/longlat.csv",header = T)
map('state',fill = T, col = brewer.pal(9,"Pastel2"))
#draws circles around a point, given lat, long and radius
plotCircle <- function(lonDec, latDec, mile) {
ER <- 3959
angdeg <- seq(1:360)
lat1rad <- latDec*(pi/180)
lon1rad <- lonDec*(pi/180)
angrad <- angdeg*(pi/180)
lat2rad <- asin(sin(lat1rad)*cos(mile/ER) + cos(lat1rad)*sin(mile/ER)*cos(angrad))
lon2rad <- lon1rad + atan2(sin(angrad)*sin(mile/ER)*cos(lat1rad),cos(mile/ER)-sin(lat1rad)*sin(lat2rad))
lat2deg <- lat2rad*(180/pi)
lon2deg <- lon2rad*(180/pi)
polygon(lon2deg,lat2deg,lty = 1 , col = alpha("blue",0.35))
}
point <- mapproject(dat$lng,dat$lat)
points(point, col = alpha("black",0.90), cex = 0.4, pch = 20) #plots points
plotCircle(-71.4868,42.990684,20)
plotCircle(-72.57085,41.707932,12)
...
#this goes on for every point
I want to store the points that intercept state borders in a new data frame, any help would be appreciated!
EDIT: Here's a broad overview of the workflow using the geospatial analyses packages in R (sp, rgdal, rgeos).
Instead of using the maps package and stateMapEnv, you want a polygon shapefile of state boundaries, like one that can be found here:
https://www.census.gov/geo/maps-data/data/cbf/cbf_state.html
You can then load that shapefile in R with readOGR from the rgdal package to get a SpatialPolygons (let's call it state_poly) with one Polygons object per state.
Create a SpatialPoints object from your long/lat coordinates:
pts <- SpatialPoints(dat[, c("lng", "lat")], proj4string = CRS("+proj=longlat"))
At this point your pts and state_poly should be in longitude/latitude coordinates, but to draw circles of a fixed radius around points, you need to convert them to projected coordinates (i.e. in meters). See this question for more details:
Buffer (geo)spatial points in R with gbuffer
Create a vector with the radii of your circles around each point, and use it with gBuffer (from rgeos) and your points layer:
circ <- gBuffer(pts, width = radii, byid = TRUE)
The byid argument means it does it separately for each point, using the different values in radii in the same order as the points.
Convert the state polygons to lines: state_lines <- as(state_poly, "SpatialLines")
Use gIntersects(circ, state_lines, byid = TRUE) .
Because of byid = TRUE, the return value is a matrix with one row per circle in your spgeom1 and one column per state boundaries in spgeom2. Note that if the circle intersect a boundary between two states, it should have two "TRUE" values in that row (one for each state). If it intersects with water or the external perimeter of the US it may have only one "TRUE" value in the row.
Here is the Final Code!
library("maps")
library("mapproj")
library("RColorBrewer")
library("mapdata")
library("ggplot2")
library("rgeos")
library("dismo")
library("ggmap")
library("rgdal")
#import shape file (.shp), make sure all the other files in the zip are included in
#your file location!
state_poly <- readOGR(dsn = 'C:/Users/chopp/Documents/R', layer='cb_2015_us_state_500k')
#data containing lng and lat coordinates with radii
data <- read.csv("R/longlat.csv", header = T)
#create spatial point objects out of your lng and lat data
pts <- SpatialPoints(data[,c("lng","lat")], proj4string = CRS("+proj=longlat"))
#convert spatial points to projected coordinates (points and map lines)
ptsproj <- spTransform(pts, CRS("+init=epsg:3347"))
state_poly_proj<- spTransform(state_poly, CRS("+init=epsg:3347"))
#convert radii units to meters, used in our gBuffer argument later on
radii <- data$rad*1609.344
#create circular polygons with. byid = TRUE will create a circle for each point
circ <- gBuffer(ptsproj, width = radii, byid = TRUE)
#convert state polygons to state lines
state_lines<- as(state_poly_proj, "SpatialLines")
#use gIntersects with byid = TRUE to return a matrix where "TRUE" represents
#crossing state boundaries or water
intdata <- gIntersects(circ, state_lines, byid = TRUE)
#write the matrix out into a csv file
write.csv(intdata,"R/Agents Intercepts 2.csv")

Resources