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")
Related
I'm trying to check whether two polygons intersect in R. When plotting, they clearly do not. When checking the intersection, rgeos::gIntersects() currently returns FALSE, while sf::intersects() returns TRUE. I imagine this is due to the polygons being (1) large and (2) close together, so something about when on a flat surface they don't appear to intersect but on a sphere they would appear to intersect?
Ideally I could keep my workflow all in sf -- but I'm wondering if there's a way to use sf::intersects() (or another sf function) that will return FALSE here?
Here's an example:
library(sf)
library(rgeos)
library(leaflet)
library(leaflet.extras)
#### Make Polygons
poly_1 <- c(xmin = -124.75961, ymin = 49.53330, xmax = -113.77328, ymax = 56.15249) %>%
st_bbox() %>%
st_as_sfc()
st_crs(poly_1) <- 4326
poly_2 <- c(xmin = -124.73214, ymin = 25.11625, xmax = -66.94889, ymax = 49.38330) %>%
st_bbox() %>%
st_as_sfc()
st_crs(poly_2) <- 4326
#### Plotting
# Visually, the polygons do not intersect
leaflet() %>%
addTiles() %>%
addPolygons(data = poly_1) %>%
addPolygons(data = poly_2)
#### Check Intersection
# returns FALSE
gIntersects(poly_1 %>% as("Spatial"),
poly_2 %>% as("Spatial"))
# returns TRUE
st_intersects(poly_1,
poly_2,
sparse = F)
Here's the polygons, which visually do not intersect.
This is an interesting problem, with the root cause being difference between planar (on a flat surface) and spherical (on a globe) geometry.
On a plane - which is a simplified approach that GEOS takes - the four corners of a polygon are connected by four straight lines, the sum of angles is 360° degrees etc. Geometry works as Euclid taught ages ago.
But, and this is crucial, this is not how the world works. On a globe the four connections of polygon are not straight lines but great circles. Or rather they are straight when drawn on a globe, and curved when rolled flat one a planar surface (such as a map or your computer screen).
Because an example is more than a 1000 words consider this piece of code:
library(sf)
library(dplyr)
# make polygons
poly_1 <- c(xmin = -124.75961, ymin = 49.53330, xmax = -113.77328, ymax = 56.15249) %>%
st_bbox() %>%
st_as_sfc()
st_crs(poly_1) <- 4326
poly_2 <- c(xmin = -124.73214, ymin = 25.11625, xmax = -66.94889, ymax = 49.38330) %>%
st_bbox() %>%
st_as_sfc()
st_crs(poly_2) <- 4326
# this is what you *think* you see (and what GEOS sees, being planar)
# = four corners connected by straight lines
# & no intersecton
mapview::mapview(list(poly_1, poly_2))
# this is what you *should* see (and what {s2} sees, being spherical)
# = four corners connected by great circles
# & an obvious intersection around the lower right corner of the small polygon
poly_1alt <- st_segmentize(poly_1, units::set_units(1, degree))
poly_2alt <- st_segmentize(poly_2, units::set_units(1, degree))
mapview::mapview(list(poly_1alt, poly_2alt))
You have two options:
accept that your thinking about polygons was wrong, and embrace spherical, i.e. {s2} logic.
This should be in theory the correct approach, but somewhat counter intuitive.
make {sf} abandon spherical approach to polygons, and force it to apply planar approach (such as GEOS uses).
This would be in theory a wrong approach, but consistent both with your planar intuition and previous behaviour of most GIS tools, including {sf} prior to version 1.0.0
# remedy (of a kind...) = force planar geometry operations
sf::sf_use_s2(FALSE)
st_intersects(poly_1, poly_2, sparse = F)
# [,1]
# [1,] FALSE
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:
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.
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)
I have a shapefile, http://census.cso.ie/censusasp/saps/boundaries/Census2011_Small_Areas_generalised20m.zip
and want to extract the long/lat, but I am not sure how to map the correct coordinate to the correct small area.
mycode is:
require(ggplot2)
require(proj4)
require(rgdal)
a=readOGR(....shp)
dublin = a[a$NUS3NAME=='Dublin',]
dublin=spTransform(dublin,CRS('=proj=longlat +ellps=WGS84 +datum=WGS84'))
b=data.frame(dublin)
sa=fortify(dublin,SA='SMALL_AREA')
pj=project(sa[,1:2],proj4string(dublin),inverse=TRUE)
latlon=data.frame(latdeg=pj$y,londeg=pj$x)
sa=data.frame(cbind(latlon,sa)
The number of unique sa$id (4500) is the same as the number of unique b$SMALL_AREA (4500 rows). How is (for example) and id of 22 mapped from sa to the correct small area in b?
there are 56k rows in sa and 4500 rows in b
Any suggestions are appreciated
I am working in R
Shapefiles are much easier to work with and understand using the sf package in R. It keeps things tidy and rectangular, with the added $geometry list-column.
For your example, getting the lat & lon for the Dublin area:
library(sf)
library(tidyverse)
a <- read_sf('Census2011_Small_Areas_generalised20m/Census2011_Small_Areas_generalised20m.shp')
# dplyr filter() works for sf objects
dublin <- a %>% filter(NUTS3NAME == 'Dublin')
# Tranform to WGS84 coordinates
dublin <- dublin %>% st_transform(st_crs(4326))
# Proof CRS has changed
st_crs(dublin)
# lat/lon coords
st_coordinates(dublin) %>% head()
In this case, the sf geometry is of MULTIPOLYGON type. Each observation has between 4 and 168 connected lat/lon points associated with it. If you are interested in a single point for each observation, the centroid might be a good approximation.
Using dublin %>% st_centroid() will return all the data, but with the $geometry column consisting of a single point. Getting just the centroid points (as a matrix) can be achieved using dublin %>% st_centroid %>% st_coordinates().
Finally, a plot of the Dublin subset of the shapefile & the respective centroid points. There are quite a few shapes in a small area, making things hard to see. In the outskirts with larger polygons the centroids should be more visible.
dublin %>%
st_centroid() %>%
ggplot() +
geom_sf( size = .4, color = '#FF7900') +
geom_sf(data = dublin,
color = '#009A49',
fill = NA,
size = .2) +
theme(panel.background = element_rect(fill = "black")) +
coord_sf(datum = NA)