Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Fillet (round) Interior Angles of SF line intersections

I am working with OSM data to create vector street maps. For the roads, I use line geometry provided by OSM and add a buffer to convert the line to geometry that looks like a road.

My question is related to geometry, not OSM, so I will use basic lines for simplicity.

library(dplyr)
library(ggplot2)
library(sf)

# two lines representing an intersection of roads
l1 <- st_as_sfc(c("LINESTRING(0 0,0 5)","LINESTRING(-5 3,5 3)"))

# union the two lines (so they "intersect" instead of overlap once buffered
l2 <- l1 %>% st_union()

ggplot()+
  geom_sf(data = st_buffer(l2, dist = 0.75))

This will yield something like this:

Intersection

Ultimately, I am trying to render roads similarly to map APIs like Google, OSM, Bing, etc. These fillet (round) the inner angles like this:

Filleted corners

I've searched through the st_ series methods in the sf package but haven't found a solution. The closest I have found is some arguments for st_buffer that control the shape of endcaps and the number of angles used for outer angles (but not the inner angles).

Is there a simple/practical solution for this, or am I better off getting used to un-rounded intersections?

Thanks

UPDATE:

Lovalery and mrhellmann present two solutions below.

Ultimately, I selected mrhellmann's response because it fit my particular need; however, I did compare and explore the merits of each which I share below. I leave this discussion here, because someone else may have a slightly different use case and the differences are useful to understand.

Let's define the different methods used:

l1 <- st_as_sfc(c("LINESTRING(0 0,0 5)","LINESTRING(-5 3,5 3)"))
l2 <- l1 %>% st_union()

l2.base <- l2 %>% st_buffer(dist = 0.75, endCapStyle = "ROUND") # OP
l2.neg <- l2.base %>% st_buffer(dist = -0.25) # mrhellmann (st_buffer)
l2.smth <- l2.base %>% smooth(method = "ksmooth", smoothness = 3, n= 50L)# Lovalery (smoothr)

Lovalery raises a good point, the negative buffer approach also smooths the ends/extremities.

Comparing the three methods shows that the negative buffer approach affects more than the endcaps, it shrinks the geometry by the distance used in st_buffer on all sides.

enter image description here

The l2.neg issue can be solved in a workaround by adding the "smoothing" value (value used in the negative buffer) to the original buffering distance (0.75 + 0.25).

l3 <- l1 %>% st_union %>% st_buffer(dist = 0.75+0.25, endCapStyle = "ROUND")
l3.neg <- st_buffer(l3, dist = -0.25)

enter image description here

With this adjustment, the methods have similar outcomes. In fact, l3 may be more similar to l2.base than l2.smth. At this point I might express a preference for the l3.neg method for the following reasons:

  1. it uses the same package (not a big deal),
  2. if appears to be slightly more true to the l2 geometry,
  3. the distance value for st_buffer (using the same units as the map's crs) might be easier for users to understand than the smoothness setting for smoothr (a factor of the mean distance between vertices -- more on this below).

HOWEVER, Lovalery's point about the accuracy of line ends raised another good point. The default (ROUND) endCapStyle used by st_buffer extends the length of the line by the buffer distance setting.

If the plot limits crops all street ends, this is irrelevant. If the ends of streets are to be rendered in the plot, they should be the correct length.

A more appropriate buffer setting is endCapStyle = FLAT. Note that the smoothness setting for l2.smth changes from smoothness = 3 to smoothness = 0.2 when not fitting a rounded endcap.

enter image description here

The line length renders as intended for the l2 and l2.smth methods. The l2.neg and l3.neg preserve the 90 deg. angles on the ends (a "nice to have" but not critical feature), but shortens the line length by the negative distance used in the smoothing buffer. Point for smoothr.

Finally, to be nitpicky, the curve provided by smoothr is not symmetrical. The greater the smoothness value, and the greater the difference in vertices lengths, the more obvious the asymmetry is. Here is the smoothr method using a smoothness = 1.

enter image description here

Note the elongation of the curves on the longer vertices. This may not be noticeable at most scales.

As Lovalery mentioned, this comes down to what's best for the individual use case. Also, will the average viewer notice if the street is one metre shorter than OSM says, or that the curve is ever so slightly asymmetrical? Precision is a funny thing like that with graphics...perfect precision probably doesn't matter, but it's math and numbers so it feels like it should.

All code from above:

library(dplyr)
library(ggplot2)
library(sf)
library(smoothr)


l1 <- st_as_sfc(c("LINESTRING(0 0,0 5)","LINESTRING(-5 3,5 3)"))#,"LINESTRING(-5 7,5 7)"))
l2 <- l1 %>% st_union() 

#### endCapStyle = ROUND ####

l2.base <- l2 %>% st_buffer(dist = 0.75, endCapStyle = "ROUND")
l2.neg <- l2 %>% st_buffer(dist = -0.25)
l2.smth <- l2 %>% smooth(method = "ksmooth", smoothness = 3, n= 50L)

l3 <- l1 %>% st_union %>% st_buffer(dist = 0.75+0.25, endCapStyle = "ROUND")
l3.neg <- st_buffer(l3, dist = -0.25)

ggplot()+
  geom_sf(data = l2.base, colour = "darkgreen", fill = "palegreen")+
  geom_sf(data = l2.neg, colour = "blue", fill = NA)+
  geom_sf(data = l2.smth, colour = "red", fill = NA)+
  geom_sf(data = l3.neg, colour = "cyan", fill = NA)+
  ggtitle("Method Comparison", 
        subtitle = "EndCapStyle ROUND: l2 (green) v l2.smth (red), l2.neg (blue), l3.neg (cyan)") +
  scale_y_continuous(breaks = c(-1:6), limits = c(-1,6))+
  scale_x_continuous(breaks = c(-6:6), limits = c(-6,6))

#### endCapStyle = FLAT ####   

l2.base <- l1 %>% st_union() %>% st_buffer(dist = 0.75, endCapStyle = "FLAT")
l2.neg <- l2.base %>% st_buffer(dist = -0.25)
l2.smth <- l2.base %>% smooth(method = "ksmooth", smoothness = 0.2, n= 50L)

l3 <- l1 %>% st_union %>% st_buffer(dist = 0.75+0.25, endCapStyle = "FLAT")
l3.neg <- st_buffer(l3, dist = -0.25)

ggplot()+
  geom_sf(data = l2.base, colour = "darkgreen", fill = "palegreen")+
  geom_sf(data = l2.neg, colour = "blue", fill = NA)+
  geom_sf(data = l2.smth, colour = "red", fill = NA)+
  geom_sf(data = l3.neg, colour = "cyan", fill = NA)+
  ggtitle("Method Comparison", 
          subtitle = "EndCapStyle FLAT: l2 (green) v l2.smth (red), l2.neg (blue), l3.neg (cyan)") +
  scale_y_continuous(breaks = c(-1:6), limits = c(-1,6))+
  scale_x_continuous(breaks = c(-6:6), limits = c(-6,6))

#### illustrate asymmetry of smoothr method as l4.smth ####

l4.smth <- l2.base %>% smooth(method = "ksmooth", smoothness = 1, n= 50L)

ggplot()+
  geom_sf(data = l2.base, colour = "darkgreen", fill = "palegreen")+
  geom_sf(data = l4.smth, colour = "red", fill = NA)+
  geom_sf(data = l3.neg, colour = "blue", fill = NA)+
  geom_abline(slope = 1, intercept = 3, colour = "gray", linetype = "dashed")+
  ggtitle("Method Comparison", 
          subtitle = "EndCapStyle FLAT: l3.neg (blue), smoothr smoothness = 1 (red)") +
  coord_sf(xlim = c(0,5), ylim = c(3,5))
like image 720
AWaddington Avatar asked Oct 15 '21 00:10

AWaddington


1 Answers

You can buffer the lines and then negative buffer that result:

ggplot()+
  geom_sf(data = st_buffer(st_buffer(l2, dist = 1), dist = -0.5))

enter image description here

More verbose, less nested:

library(dplyr)
library(ggplot2)
library(sf)

# two lines representing an intersection of roads
l1 <- st_as_sfc(c("LINESTRING(0 0,0 5)","LINESTRING(-5 3,5 3)"))

# union the two lines (so they "intersect" instead of overlap once buffered
l2 <- l1 %>% st_union()

# buffer the lines with a positive dist argument
l2_buffered <- st_buffer(l2, dist = 1)

# un-buffer (negative buffer?) the lines for rounded inner corners
#  with a negative dist argument
l2_unbuffered <- st_buffer(l2_buffered, dist = -0.5)

Both together, the red de-buffered polygon is overplotted by the black except in the curved interior angles:

ggplot()+
  geom_sf(data = st_buffer(st_buffer(l2, dist = 1.5), dist = -0.75), fill = NA, col = 'red') + 
  geom_sf(data = st_buffer(l2, dist = .75), fill = NA, col = 'black')

enter image description here

Zoomed in on one corner:

ggplot()+
  geom_sf(data = st_buffer(st_buffer(l2, dist = 1.5), dist = -0.75), fill = NA, col = 'red') + 
  geom_sf(data = st_buffer(l2, dist = .75), fill = NA, col = 'black') + 
  coord_sf(xlim = c(-2,0), ylim = c(1,3))

enter image description here

like image 129
mrhellmann Avatar answered Oct 03 '22 02:10

mrhellmann