I draw a map in which points are represented as polygons. The points are found close to a southern state border. The code is:
library(leaflet)
library(sf)
long <- c( 4.676119175, 4.53172103 , 4.939782877, 5.074127987, 5.072757119)
lat <- c(51.477299959, 51.589766239, 51.624436295, 51.520707997, 51.631483055)
labs <- c("A", "B", "C", "D", "E")
colors <- rainbow(length(labs))
df <- data.frame(ID = labs, X = long, Y = lat)
points <- st_geometry(st_as_sf(df, coords = c("X", "Y")))
points0 <- st_set_crs(points, 4326)
area <- rnaturalearth::ne_countries(country = c('netherlands'), scale = 'large', returnclass = 'sf')
polys <- points %>% st_union() %>% st_voronoi() %>% st_cast() %>% st_set_crs(., 4326)
polys <- polys[unlist(st_intersects(points0, polys))] %>% st_intersection(y = area)
leaflet() %>%
addProviderTiles(providers$Esri.WorldGrayCanvas) %>%
addPolygons (data = polys,
fillColor = colors,
fillOpacity = 1,
weight = 0.5,
color = "black") %>%
addCircleMarkers(lng = long,
lat = lat,
label = labs,
color = "black",
radius = 5,
weight = 1,
fill = TRUE,
fillColor = colors,
fillOpacity = 1)
The results looks like this:
The problem is that the polygons get stretched out to the north way to much. This may be solved by setting a bounding box, but I would prefer to solve this by setting a maximum size (or radius) for the polygons. How exactly can I set the maximum radius?
I suggest adding another call of sf::st_intersection() with an object of buffered points.
You have two alternatives:
intersect with a buffer of a single lab - number C seems a good candidate, as it is placed in the middle
intersect with pairwise buffers of each lab; purrr:map2() will be your friend here for pairwise intersection
I find the results of a single intersection more visually pleasing, but this may be not the most important factor so make your own choice...
As a comment: my natural earth is a bit buggy at the moment, so I am using GISCO by Eurostat as a source of map of the Netherlands instead; in a high resolution because I really dig the tiny exclaves of Belgium...
library(leaflet)
library(rnaturalearth)
library(sf)
long <- c( 4.676119175, 4.53172103 , 4.939782877, 5.074127987, 5.072757119)
lat <- c(51.477299959, 51.589766239, 51.624436295, 51.520707997, 51.631483055)
labs <- c("A", "B", "C", "D", "E")
colors <- rainbow(length(labs))
points <- data.frame(ID = labs, X = long, Y = lat) %>%
st_as_sf(coords = c("X", "Y"), crs = 4326) %>%
mutate(ID = ordered(ID))
area <- giscoR::gisco_get_countries(country = "NL", resolution = "01") # my Natural Earth is buggy at the momemt
# intersection with a single buffer around C
polys <- points %>%
st_union() %>%
st_voronoi() %>%
st_cast() %>%
st_set_crs(., 4326) %>%
st_intersection(area) %>%
st_intersection(st_buffer(dplyr::filter(points, ID == "C"), units::as_units(30000, "m"))) %>%
st_as_sf() %>%
st_join(points) # add back labs id's
palette <- colorFactor(palette = colors,
domain = polys$ID)
leaflet(data = polys) %>%
addProviderTiles("Esri.WorldGrayCanvas") %>%
addPolygons(fillColor = ~palette(ID),
stroke = F) %>%
addCircleMarkers(data = points,
fillColor = ~palette(ID),
stroke = F,
fillOpacity = 1)
# intersection with pairwise buffers
polys <- points %>%
st_union() %>%
st_voronoi() %>%
st_cast() %>%
st_set_crs(., 4326) %>%
st_intersection(area) %>%
purrr::map2(st_geometry(st_buffer(points, units::as_units(30000, "m"))), st_intersection) %>%
st_as_sfc(crs = 4326) %>%
st_as_sf() %>%
st_join(points) # add back labs id's
mapview::mapview(polys)
Related
I've got some GPS data (latlong) and I want to plot the GPS points and their connecting lines and color both by the time difference between the two GPS points. I've figured out how to color the points and convert the points to a LINESTRING but I can't figure out how to recolor the scale of the line.
I saw this post:
Color portions of sf LINESTRING by variable that shows how to break the linestrings into segments and color the segments by a categorical variable but as I have close to 100,000 observations I'd like to avoid just splitting my plot up into 99,999 pieces and also, my data is continuous.
Here's some toy data:
# Create some data points
fake_data = data.frame(Time = 1:6,
Long = c(-90.46200, -90.46160, -90.46170, -90.46150, -90.46100, -90.46240),
Lat = c(33.88540, 33.88750, 33.88520, 33.88340, 33.88540, 33.88150))
# Define as points
points = st_as_sf(fake_data, coords = c("Long", "Lat"), crs = 4326, remove = FALSE)
# Connect the dots
lines = points %>% summarize(do_union = FALSE) %>% st_cast("LINESTRING")
library(ggplot2)
# Plot
ggplot(data = points)+
geom_sf(aes(color = as.numeric(points$Time)))+
geom_sf(data = lines)+#, aes(color = numeric(points$Time[1:(length(points$Time)-1)])))+ #did not work
ylim(c(33.87, 33.89))+
xlim(c(-90.47, -90.45))+
scale_color_gradient(name = "Time", position="bottom" , low = "blue", high = "red")
Thank you!
I'm confident there are prettier ways to do this, but this works!
I needed to add in a group variable to use to generate linegroups. This was inspired by: https://stackoverflow.com/a/48979401/3642716 and their answer with how to solve for troops in the tidyverse dataset.
library(sf)
library(dplyr)
library(ggplot2)
# Create some data points
fake_data = data.frame(Time = 1:6,
Long = c(-90.46200, -90.46160, -90.46170, -90.46150, -90.46100, -90.46240),
Lat = c(33.88540, 33.88750, 33.88520, 33.88340, 33.88540, 33.88150),
group = 1)
# Define as points
points = st_as_sf(fake_data, coords = c("Long", "Lat"), crs = 4326, remove = FALSE)
# Connect the dots
lines <- fake_data
lines %<>% group_by(group) %>%
slice(rep(1:n(), each = 2)) %>%
slice(-c(1, n())) %>%
mutate(linegroup = lapply(1:(n()/2), function(x) rep(x, 2)) %>% unlist) %>%
ungroup %>%
group_by(linegroup) %>%
st_as_sf(coords = c("Long","Lat"), crs = 4326, remove = F) %>%
summarize( do_union = F) %>%
st_cast("LINESTRING")
# Plot
ggplot(data = points)+
geom_sf(aes(color = `Time`))+
geom_sf(data = lines, aes(color = `linegroup`))+#, aes(color = numeric(points$Time[1:(length(points$Time)-1)])))+ #did not work
ylim(c(33.881, 33.888))+
xlim(c(-90.463, -90.460))+
scale_color_gradient(name = "Time", position="bottom" , low = "blue", high = "red")
Looks like this:
I have a dataset with several hundred geographical points expressed as lat/long values that I plot as dots using tm_dots, on top of some boundaries that I plot using tm_shape (both using tmap).
Does anyone know of a way that I can draw polygons to represent areas within the boundaries of the underlying layer that are not within 500 metres of any of the points plotted? I'd be happy to use other R mapping resources (e.g. ggplot/ggmap) if better for this task.
Current code is:
#Call necessary packages
library(tidyverse)
library (readxl)
library(maptools)
library(classInt)
library(RColorBrewer)
library(sf)
library(tmap)
library(scales)
library(tmaptools)
library(geodata)
#Read in boundary polygon data
#This shape file is from https://www.data.gov.uk/dataset/2cf1f346-2f74-4c06-bd4b-30d7e4df5ae7/middle-layer-super-output-area-msoa-boundaries
shp_name <- "//ims.gov.uk//homedrive//users//JW2002//My Documents//Data//Demography, Mapping & Lookups//Shape Files//East of England//MSOA//Middle_Layer_Super_Output_Areas_December_2011_Generalised_Clipped_Boundaries_in_England_and_Wales.shp"
EofEMSOAs <- st_read(shp_name)%>%
st_as_sf()
#Read deprivation data from another source (not specifically relevant to the mapping section of this project but provides list for subsequent subset to East of England MSOAs only)
EofEMSOAsIMD <- read_excel("~/Data/Demography, Mapping & Lookups/IoD/National & EofE IoD 2019/National&IoD 2019 MSOAs.xlsx",
sheet = "East of England MSOAs")
#Subset MSOA list to East of England Only
EofEMSOAsCodeListOnly <- dplyr::pull(EofEMSOAsIMD, "Area Code")
EofEMSOAsCodeListOnly <- paste(EofEMSOAsCodeListOnly, collapse = '|')
EofEMSOAsFinalList <- EofEMSOAs[grep(EofEMSOAsCodeListOnly, EofEMSOAs$msoa11cd),]
#Generate point data
PointData <- read.table(textConnection("ID Latitude Longitude
A 52.9742585 0.5526301
B 52.972643 0.8495693
C 52.972643 0.8495693
D 51.46133804 0.36403501"), header=TRUE)
#Geocode the point list
PointDataPlotted = st_as_sf(PointData, coords = c('Longitude', 'Latitude'), crs = 4326)
#Remove geometry
PointDataPlotted2 <- PointDataPlotted %>%
as.data.frame() %>%
mutate(buffer = st_buffer(geometry, dist = 5000)) %>%
select(-geometry) %>%
st_as_sf()
#Create union shape of polygons
union <- st_union(EofEMSOAsFinalList)
# generate bounding box
mask_union <- union %>% as_tibble() %>%
mutate(bbox = st_as_sfc(st_bbox(c(xmin = -5.5, xmax = 9, ymax = 51.5, ymin = 42), crs = st_crs(4326)))) %>%
st_as_sf()
# compute difference between bounding box and union polygon to
# use as mask in the final layer
diff <- st_difference(mask_union$bbox, mask_union$geometry)
# Build map
OutputMap <-
# plot only shapes filled red
tm_shape(EofEMSOAsFinalList) +
tm_fill(col = "red") +
# plot only buffer zones of each point in green
tm_shape(PointDataPlotted2)+
tm_fill(col = "forestgreen") +
# add mask
tm_shape(diff) +
tm_fill(col = "white") +
# plot borders of shape
tm_shape(EofEMSOAsFinalList) +
tm_borders(col = "white",
lwd = 1,
lty = "solid") +
# add custom legend
tm_add_legend(type = "symbol",
labels = c("Restricted", "Public"),
col = c("red", "forestgreen"),
title = "Access type",
size = 1.5,
shape = 21)
Here's a solution to find areas within the boundaries of the underlying layer that are within 50 km of any of the points plotted.
library(tidyverse)
library(sf)
library(geodata)
# example polygons of France
polygon <- gadm(country = "FRA", level = 1, path = tempdir()) %>%
st_as_sf() %>%
filter(NAME_1 != "Corse")
# get 100 sample points within union shape of polygons
set.seed(42)
union <- st_union(polygon)
points <- st_sample(x = union, size = 100, type = "random") %>%
as.data.frame() %>%
mutate(id = row_number()) %>% # add an id for later joining
st_as_sf() %>%
# calculate aound each point a buffer zone of 50km
mutate(buffer = st_buffer(geometry, dist = 50000))
# add for each point the polygon (state) in which it is located
points <- st_join(points, polygon, join = st_within) %>%
as.data.frame() %>%
dplyr::select(id, NAME_1) %>%
left_join(points) %>%
filter(NAME_1 != "Corse")
# for each polygon calculate the union shapes of the
# corresponding buffers zones within
points_buff_union <- points %>%
dplyr::select(-geometry) %>%
st_as_sf() %>%
group_by(NAME_1) %>%
summarise()
# plot content
polygon %>%
ggplot() +
geom_sf(data = points_buff_union, aes(geometry = buffer, fill = NAME_1)) +
geom_sf(fill = NA) +
scale_fill_brewer(palette = "Paired") +
geom_sf(data = points, aes(geometry = geometry), color = "black", size = .5)
From this point I guess it's easy to find the areas not within XX meters of any of the points plotted.
If you want to find area across the underlying polygons you can simply use the following (blue areas are within 50 km of any of the points plotted while red areas don't):
# calculate union shape for all buffers
points_buff_union <- points %>%
filter(NAME_1 != "Corse") %>%
dplyr::select(-geometry) %>%
st_as_sf() %>%
summarise()
# generate bounding box
mask_union <- union %>% as_tibble() %>%
mutate(bbox = st_as_sfc(st_bbox(c(xmin = -5.5, xmax = 9, ymax = 51.5, ymin = 42), crs = st_crs(4326)))) %>%
st_as_sf()
# compute difference between bounding box and union polygon to
# use as mask in the final layer
diff <- st_difference(mask_union$bbox, mask_union$geometry)
# plot content
polygon %>%
ggplot() +
geom_sf(fill = "red3") +
geom_sf(data = points_buff_union, aes(geometry = buffer), fill = "lightblue") +
geom_sf(data = points, aes(geometry = geometry), color = "black", size = .5) +
geom_sf(fill = NA) +
geom_sf(data = diff, fill = "white")
Of course you can plot the individual layers computed with sf also using tmap:
library(tidyverse)
library(sf)
library(geodata)
library(tmap)
# example polygons of France
EofEMSOAs <- gadm(country = "FRA", level = 1, path = tempdir()) %>%
st_as_sf() %>%
filter(NAME_1 != "Corse")
# get 100 sample points within union shape of polygons
set.seed(42)
union <- st_union(EofEMSOAs)
PointDataPlot <- st_sample(x = union, size = 100, type = "random") %>%
as.data.frame() %>%
# calculate around each point a buffer zone of 50km
mutate(buffer = st_buffer(geometry, dist = 50000)) %>%
select(-geometry) %>%
st_as_sf()
# generate bounding box
mask_union <- union %>% as_tibble() %>%
mutate(bbox = st_as_sfc(st_bbox(c(xmin = -5.5, xmax = 9, ymax = 51.5, ymin = 42), crs = st_crs(4326)))) %>%
st_as_sf()
# compute difference between bounding box and union polygon to
# use as mask in the final layer
diff <- st_difference(mask_union$bbox, mask_union$geometry)
# Build map
OutputMap <-
# plot only shapes filled red
tm_shape(EofEMSOAs) +
tm_fill(col = "red") +
# plot only buffer zones of each point in green
tm_shape(PointDataPlot)+
tm_fill(col = "forestgreen") +
# add mask
tm_shape(diff) +
tm_fill(col = "white") +
# plot borders of shape
tm_shape(EofEMSOAs) +
tm_borders(col = "white",
lwd = 1,
lty = "solid") +
# add custom legend
tm_add_legend(type = "symbol",
labels = c("Restricted", "Public"),
col = c("red", "forestgreen"),
title = "Access type",
size = 1.5,
shape = 21)
UPDATE using UK data
#Call necessary packages
library(tidyverse)
library (readxl)
library(maptools)
library(classInt)
library(RColorBrewer)
library(sf)
library(tmap)
library(scales)
library(tmaptools)
library(geodata)
# Read in boundary polygon data
EofEMSOAs <- st_read("MSOA_EngWal_Dec_2011_Generalised_ClippedEW_0/Middle_Layer_Super_Output_Areas_December_2011_Generalised_Clipped_Boundaries_in_England_and_Wales.shp")%>%
st_as_sf(crs = 4326) %>%
st_make_valid() %>%
# use only a subset of the data
st_crop(c(xmin = 550000, ymin =320000, xmax = 600000, ymax = 360000))
# Generate point data
PointData <- read.table(textConnection("ID Latitude Longitude
A 52.9742585 0.5526301
B 52.972643 0.8495693
C 52.972643 0.8495693
D 51.46133804 0.36403501"), header=TRUE)
# Geocode the point list
PointDataPlotted = st_as_sf(PointData, coords = c('Longitude','Latitude'), crs = 4326)
# Remove geometry
PointDataPlotted2 <- PointDataPlotted %>%
as.data.frame() %>%
mutate(buffer = st_buffer(geometry, dist = 5000)) %>%
select(-geometry) %>%
st_as_sf(crs = 4326)
# Create union shape of polygons
union <- st_union(EofEMSOAs)
# generate bounding box
mask_union <- union %>% as_tibble() %>%
mutate(bbox = st_as_sfc(st_bbox(geometry), crs = 4326)) %>%
st_as_sf()
# compute difference between bounding box and union polygon to
# use as mask in the final layer
diff <- st_difference(mask_union$bbox, mask_union$geometry)
# Build map
OutputMap <-
# plot only shapes filled red
tm_shape(EofEMSOAs) +
tm_fill(col = "red") +
# plot only buffer zones of each point in green
tm_shape(PointDataPlotted2)+
tm_fill(col = "forestgreen") +
# add mask
tm_shape(diff) +
tm_fill(col = "white") +
# plot borders of shape
tm_shape(EofEMSOAs) +
tm_borders(col = "white",
lwd = 1,
lty = "solid") +
# add custom legend
tm_add_legend(type = "symbol",
labels = c("Restricted", "Public"),
col = c("red", "forestgreen"),
title = "Access type",
size = 1.5,
shape = 21)
I am making a voronoi map in R using the packages leaflet and sf as follows:
library(leaflet)
library(sf)
library(rnaturalearth)
library(rnaturalearthdata)
long <- c(4.35556 , 5.83745, 4.63683 , 6.06389, 6.41111, 5.639722)
lat <- c(52.00667, 53.09456, 52.38084 , 52.475 , 52.15917, 53.440278)
labs <- c("Delft" , "Grouw" , "Haarlem", "Hattem", "Lochem", "Hollum" )
colors <- c("red" , "orange", "yellow" , "green" , "blue" , "purple" )
df <- data.frame(ID = labs, X = long, Y = lat)
points <- st_geometry(st_as_sf(df, coords = c("X", "Y")))
points <- st_union(points)
NL <- ne_countries(country = c('netherlands'), scale = 'medium', returnclass = 'sf')
polys <- points %>%
st_voronoi() %>%
st_cast() %>%
st_set_crs(., 4326) %>%
st_intersection(y = NL)
leaflet() %>%
addProviderTiles(providers$OpenStreetMap.Mapnik) %>%
addPolygons (data = polys,
fillColor = colors,
fillOpacity = 1,
weight = 0.5,
color = "black") %>%
addCircleMarkers(lng = long,
lat = lat,
label = labs,
color = "black",
radius = 5,
weight = 1,
fill = TRUE,
fillColor = colors,
fillOpacity = 1)
In the resulting map the colors of the dots are correct, but the colors of the polygons are not correct. I guess something has changed in order of the locations in 'polys', but I am puzzled about this. Any suggestions how to solve this?
st_voronoi() indeed appears not to keep the order of input points in the resulting polygons. You may use st_intersects() to find out which polygon belongs to which point and reorder polys accordingly.
First store a copy of points before applying st_union() and set them the same CRS as polys will have, so thatst_intersects() works later on. I.e., insert this before the points <- st_union(points) line:
points0 <- st_set_crs(points, 4326)
Then, after creating polys, reorder them like this:
polys <- polys[unlist(st_intersects(points0, polys))]
If some point is located outside the area of Netherlands (as provided by ne_countries()) the matching of points to polygons has to be done before intersecting of polys and NL. So in the original code the polys <- points... will be replaced with:
polys <- points %>%
st_voronoi() %>%
st_cast() %>%
st_set_crs(., 4326)
polys <- polys[unlist(st_intersects(points0, polys))]
polys <- st_intersection(polys, NL)
I'm hoping to create a series of quartered circles (i.e. circles split into 4 equal quadrants), each with a 50km radius, that I can map onto various longitudes and latitudes throughout the United States. I'd also like the option to rotate these quartered circles as desired.
Using the code below (and guidance from here), I've been able to make the following start:
New York State Map
I have two questions:
How can I meaningfully set the radius of these circles? Is there a way to draw shapes a certain distance (in km) from a coordinate in a projected CRS? So far I'm defining the radius in terms of degrees of longitude and latitude, but distance would be more useful.
My circles appear to be turning into ellipses after projecting them and mapping them in WGS84. Is there any way to prevent this from happening?
I would be happy to consider alternative approaches. Thanks!
library(sf)
library(ggplot2)
library(maps)
#Two functions to create coordinate quartered circle polygons
#x = long, y = lay, r = radius, theta_rotate = rotation
st_wedge <- function(x,y,r,start,width, theta_rotate){
n <- 20
theta = seq(start+theta_rotate, start+width+theta_rotate, length=n)
xarc = x + r*sin(theta)
yarc = y + r*cos(theta)
xc = c(x, xarc, x)
yc = c(y, yarc, y)
st_polygon(list(cbind(xc,yc)))
}
st_wedges <- function(x, y, r, nsegs, theta_rotatex){
width = (2*pi)/nsegs
starts = (1:nsegs)*width
polys = lapply(starts, function(s){st_wedge(x,y,r,s,width, theta_rotatex)})
#Cast to crs 4326, WGS84
mpoly = st_cast((st_sfc(polys, crs = 4326)), "MULTIPOLYGON")
mpoly
}
#Create quartered sf circle polygon
custom_circle_sf <- st_wedges(x = -76, y = 43, r = .3, nsegs = 4, theta_rotatex = 200) %>%
st_sf() %>%
mutate(group = row_number()) %>% dplyr::select(group, geometry)
#Create New York State sf polygon
ny_map_sf <- map_data("state", region="new york") %>%
st_as_sf(coords = c("long", "lat"), crs = 4326) %>%
group_by(group) %>%
summarise(geometry = st_combine(geometry)) %>%
st_cast("POLYGON")
#Plot results
ggplot() +
geom_sf(data=ny_map_sf,
size = 1,
colour = "blue",
fill = "white") +
geom_sf(data=custom_circle_sf,
size = .1,
aes(fill=group),
colour = "white")
For anyone who is curious about splitting polygons in sf using R, this was how I went about solving this:
#Function to create circle with quadrants. Save desired projection as projected_crs
create_circle <- function(lat_x, long_y, theta_x, buffer_m){
#Create circle with radius buffer_m centered at (lat_x, long_y)
circle_buffer <- st_point(c(lat_x, long_y)) %>% st_sfc(crs = 4326) %>%
st_cast("POINT") %>%
st_transform(projected_crs) %>%
st_buffer(buffer_m)
#Create two orthogonal lines at origin
p1 <- rbind(c(lat_x,long_y - 1), c(lat_x,long_y + 1))
p2 <- rbind(c(lat_x+1,long_y), c(lat_x-1,long_y))
mls <- st_multilinestring(list(p1,p2)) %>% st_sfc(crs = 4326) %>%
st_transform(projected_crs)
#Use orthogonal lines to split circle into 4 quadrants
x1 <- st_split(circle_buffer, mls)
#Convert origin into projected CRS
center_in_crs <- st_point(c(lat_x, long_y)) %>%
st_sfc(crs = 4326) %>%
st_transform(projected_crs)
sp_obj <- x1 %>% st_collection_extract(type="POLYGON") %>%
#Convert to spatial to use sp functions
as_Spatial() %>%
#rotate x degrees
elide(rotate = theta_x + 45, center = center_in_crs[[1]]) %>%
#return to sf
st_as_sf()
Regarding your question 2: "circles appear to be turning into ellipses". If you add to your ggplot the coord_equal() function then the grid will be square, and the ellipses will be shown as circles.
I would like to calculate the mean distance as well as the widest distance between two lines. I know how to find the minimum distance using st_distance() function, but I'm not sure how to find the other two metrics. The red lines are what I believe I would need to measure to find the mean and widest distances between the two lines.
Attached is some example data.
pts1<- data.frame(
x= c(-103.485342, -103.482808),
y = c(31.348758, 31.376947))
) %>%
sf::st_as_sf(coords = c("x","y"))
st_crs(pts1)<- "+init=epsg:2257"
pts2<- data.frame(
x= c(-103.492822, -103.484231),
y = c(31.348181, 31.377191))
) %>%
sf::st_as_sf(coords = c("x","y"))
st_crs(pts2)<- "+init=epsg:2257"
a <- pts1 %>% st_coordinates() %>% st_linestring()
b<- pts2 %>% st_coordinates() %>% st_linestring()
min_dist<-st_distance(a,b,by_element = T)
See second example below. From the image i calculate ~300 meters from the perpendicular line which cross both lines at its maximum.
pts1 <- data.frame(x = c(-103.485342, -103.482808),
y = c(31.348758, 31.376947)) %>%
sf::st_as_sf(coords = c("x","y"))
st_crs(pts1) <- "+proj=longlat +datum=WGS84"
pts1<- st_transform(pts1,"+init=epsg:2257")
pts2 <- data.frame(x = c(-103.492812, -103.484231),
y = c(31.318181, 31.377991)) %>%
sf::st_as_sf(coords = c("x","y"))
st_crs(pts2) <- "+proj=longlat +datum=WGS84"
pts2<- st_transform(pts2,"+init=epsg:2257")
a <- pts1 %>% st_coordinates() %>% st_linestring()
b <- pts2 %>% st_coordinates() %>% st_linestring()
st_distance(pts1, pts2, by_element = T)
I'll give you an insight, maybe this is not a complete answer you would imagine.
As lines are made of points, if you just make two slightly changes, you can have not only min distance, but also max distance by doing the same thing you did with the lines, but with the points from which the lines are comprised.
library(sf)
#> Linking to GEOS 3.8.0, GDAL 2.4.2, PROJ 6.2.1
pts1 <- data.frame(x = c(-103.485342, -103.482808),
y = c(31.348758, 31.376947)) %>%
sf::st_as_sf(coords = c("x","y"))
st_crs(pts1) <- "+init=epsg:2257"
pts2 <- data.frame(x = c(-103.492822, -103.484231),
y = c(31.348181, 31.377191)) %>%
sf::st_as_sf(coords = c("x","y"))
st_crs(pts2)<- "+init=epsg:2257"
a <- pts1 %>% st_coordinates() %>% st_linestring()
b <- pts2 %>% st_coordinates() %>% st_linestring()
st_distance(pts1, pts2, by_element = T)
#> Units: [US_survey_foot]
#> [1] 0.007502222 0.001443768
For having the mean distance, I am not sure if this is what you want, but I think you can obtain the centroid of both lines and then process st_distance.
ca <- st_centroid(a)
cb <- st_centroid(b)
st_distance(ca, cb, by_element = T)
#> [1] 0.004454613
EDIT: my last try based on comment
I think you can maybe have what you're trying to have if you find the longest line (b in your example), than process and find the longest distance between the points of the shorter line and the longest line itself:
(I also made some changes to your original code to make it works)
library(sf)
pts1 <- data.frame(x = c(-103.485342, -103.482808),
y = c(31.348758, 31.376947)) %>%
st_as_sf(coords = c("x","y")) %>%
st_set_crs(4326) %>%
st_transform(2257)
pts2 <- data.frame(x = c(-103.492812, -103.484231),
y = c(31.318181, 31.377991)) %>%
st_as_sf(coords = c("x","y")) %>%
st_set_crs(4326) %>%
st_transform(2257)
a <- pts1 %>%
st_union(.) %>%
st_cast(to = "LINESTRING")
b <- pts2 %>%
st_union(.) %>%
st_cast(to = "LINESTRING")
longest <- ifelse(test = st_length(a) > st_length(b),
yes = quote(a),
no = quote(b))
max(st_distance(pts1, eval(longest)))
#> 955.7374 [US_survey_foot]