如何圆角(圆形)SF 线交叉点的内角

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

我正在使用 OSM 数据创建矢量街道地图。对于道路,我使用 OSM 提供的线几何图形并添加一个缓冲区以将线转换为看起来像路的几何图形。

我的问题与几何有关,与 OSM 无关,因此为简单起见,我将使用基本线。

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))

这将产生如下结果:

最终,我正在尝试渲染道路,类似于 Google、OSM、Bing 等地图 API。这些圆角(圆形)内角如下所示:

我在sf 包中搜索了st_ 系列方法,但没有找到解决方案。我发现最接近的是 some arguments for st_buffer,它控制端盖的形状和用于外角(但不是内角)的角度数。

是否有 simple/practical 解决方案,或者我最好习惯于非圆形交叉路口?

谢谢

更新:

Lovalery 和 mrhellmann 在下面提出了两个解决方案。

最终,我选择了 mrhellmann 的回复,因为它符合我的特殊需要;但是,我确实比较并探索了我在下面分享的每一个的优点。我把这个讨论留在这里,因为其他人的用例可能略有不同,这些差异有助于理解。

让我们来定义使用的不同方法:

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 提出了一个很好的观点,负缓冲方法也平滑了 ends/extremities。

比较这三种方法表明,负缓冲区方法影响的不仅仅是端盖,它在所有方面都将几何体缩小了 st_buffer 中使用的距离。

l2.neg 问题可以通过将“平滑”值(负缓冲区中使用的值)添加到原始缓冲距离 (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)

通过这种调整,这些方法具有相似的结果。事实上,l3 可能比 l2.smth 更类似于 l2.base。在这一点上,我可能会出于以下原因表达对 l3.neg 方法的偏好:

  1. 它使用相同的包(没什么大不了的),
  2. 如果看起来更符合 l2 几何形状,
  3. st_buffer 的距离值(使用与地图的 crs 相同的单位)可能比 smoothr 的平滑度设置(顶点之间的平均距离的一个因素)更容易让用户理解-- 更多内容见下文)。

然而,Lovalery 关于线端准确性的观点提出了另一个好观点。 st_buffer 使用的默认 (ROUND) endCapStyle 通过缓冲距离设置扩展线的长度。

如果地块限制在所有街道尽头种植作物,这无关紧要。如果要在图中渲染街道的尽头,它们应该是正确的长度。

更合适的缓冲区设置是endCapStyle = FLAT。请注意,当不适合圆形端盖时,l2.smth 的平滑度设置从 smoothness = 3 更改为 smoothness = 0.2

行长按照 l2 和 l2.smth 方法的预期呈现。 l2.neg 和 l3.neg 保留 90 度。末端的角度(“很高兴拥有”但不是关键特征),但通过平滑缓冲区中使用的负距离缩短线长。点为 smoothr.

最后,吹毛求疵,smoothr提供的曲线不是对称的。 smoothness值越大,顶点长度差异越大,不对称性越明显。这是使用 smoothness = 1.

smoothr 方法

注意曲线在较长顶点处的伸长。这在大多数比例下可能并不明显。

正如 Lovalery 所提到的,这归结为最适合个人用例的内容。此外,如果街道比 OSM 所说的短一米,或者曲线稍微不对称,普通观众会注意到吗?精度是一件有趣的事情,就像图形一样......完美的精度可能并不重要,但它是数学和数字,所以感觉应该如此。

以上所有代码:

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))

您可以缓冲线条,然后对结果进行负缓冲:

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

更冗长,更少嵌套:

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)

除了弯曲的内角外,红色的去缓冲多边形被黑色过度绘制:

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')

放大一个角:

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))

我建议采用一种替代方法来替代@mrhellmann 提出的方法。因此,您将能够选择最适合您的目标。

我建议您使用 smoothr 包平滑缓冲的多边形。与@mrhellmann 的解决方案的唯一区别是多边形的 ends/extremities 也被平滑化,因此它们在该点不完全重叠原始多边形边界,正如您将在下面的 reprex 中看到的那样(尽管我是不确定这种差异对您的目的是否重要)

Reprex

library(sf)
library(smoothr) 

# 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()
  • 缓冲平滑

当然可以修改算法和参数来控制类型 和平滑度以满足您的需求,请参见。 ?smooth.

l2_smooth <- smooth(st_buffer(l2, dist = 0.75), 
                    method = "ksmooth", smoothness = 3, n = 50L)
  • 平滑多边形概述
ggplot2::ggplot()+
  ggplot2::geom_sf(data = l2_smooth)

  • 平滑后的多边形(红色)与原始多边形(黑色)的一般比较
ggplot2::ggplot()+
  ggplot2::geom_sf(data = l2_smooth, col = "red", fill = NA) + 
  ggplot2::geom_sf(data = st_buffer(l2, dist = .75),  col = 'black', fill = NA)

  • 放大一角
ggplot2::ggplot()+
  ggplot2::geom_sf(data = l2_smooth, col = "red", fill = NA) + 
  ggplot2::geom_sf(data = st_buffer(l2, dist = .75),  col = 'black', fill = NA) +
  ggplot2::coord_sf(xlim = c(-1.2,-0.6), ylim = c(1.9,2.4))

  • 放大一肢
ggplot2::ggplot()+
  ggplot2::geom_sf(data = l2_smooth, col = "red", fill = NA) + 
  ggplot2::geom_sf(data = st_buffer(l2, dist = .75),  col = 'black', fill = NA) +
  ggplot2::coord_sf(xlim = c(-6,-4), ylim = c(2,4))

reprex package (v2.0.1)

于 2021-10-16 创建

EDIT/UPDATE

首先,非常感谢@AWaddington 对不同建议解决方案的深入报告:你的、@mrhellmann 的回答和我的。能有这样有建设性的交流总是好的。

所以,正如你邀请我做的那样,这个 edit/update 的目的是澄清和改进我提出的方法,并给你我对不同方法的比较分析。

我同意你的论点,以及 mrhellmann 的以下几点观点:

  1. The solution proposed by mrhellmann does not indeed imply any additional package (which can be an advantage in some cases). On this point I can't "fight" and I gladly bow ;-)
  1. The difference you point out concerning the end of the segments between the argument endCapStyle = "ROUND" and endCapStyle = "FLAT" does exist concerning the preservation (or not) of the exact length of the segments. In terms of cartographic representation "we are however in the thickness of the line" (except, of course, at a very large – and unlikely – scale). That said, let's be conservative and keep the actual length: it's always better if for some reason (geocomputation) you need the exact length to the nearest meter. Therefore in the reprex below, I have only used the argument endCapStyle = FLAT considering that this option is suitable for all cases.

但是,可以更正平滑方法,使您提出的第 2 点和第 3 点变得无效:

Regarding point 2, as you will see in the reprex below, one can even bring the rounding of the internal angle even closer to the right angle than with the solution proposed by mrhellman. And it is easy to choose the shape of the rounding by varying the smoothness argument of the smooth function: the closer it is to 0, the closer the rounding is to the right angle. On the other hand, concerning the symmetry problem that you rightly raised, you just need to increase the number of nodes/points before smoothing. Indeed, the smoothing methods rely on these nodes/points and if they are not evenly/symmetrically distributed on each side of the angles, the smoothing itself cannot be symmetrical. To fix this, simply increase the number of points (in the reprex below, I have parametrized the function to insert a node/point every meter).

Finally, since it is possible to insert additional nodes/points in map units (here, in the reprex, every meter) the difference highlighted in point 3 no longer seems valid.

总而言之,mrhellmann 的方法和我的方法之间的其余差异似乎非常微弱,我认为任何一种解决方案都可能是合适的。

mrhellman 的做法:

Pros: no additional package, conservation of right angles at the ends of the segments.

Cons: less easy to adjust the rounding of the internal angle (to my opinion!)

爱情'方法:

Pros: easier to adjust the rounding of the internal angle.

Cons: an additional package, slightly rounded corners at the ends of the segments (but not visible at a normal map scale)

Reprex

注意:只为这个 reprex 保留了 endCapStyle = "FLAT" 选项(参见上面的解释)

  • 使用解决方案 l2.base 和 l3.neg 进行比较
library(sf)
library(smoothr)

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 = "FLAT") # OP FLAT version

l3 <- l1 %>% st_union %>% st_buffer(dist = 0.75+0.25, endCapStyle = "FLAT") # mrhellmann (st_buffer) modified, FLAT version
l3.neg <- st_buffer(l3, dist = -0.25)
  • 平滑你的缓冲区(即l2.base),同时尊重内角的对称性= lovalery(smoothr)修改,平面版本
# 1. Densify the number of nodes/points (here, one node every meters, cf. max_distance argument)
l3.smth.dens <- smooth(st_buffer(l2, dist = 0.75,endCapStyle = "FLAT"), method = "densify", max_distance = 1)

# 2. Smooth with kernel method (as in my first post). Test of two smoothness indices (i.e. 0.2 and 1)
l3.smth.dens1 <- smooth(l3.smth.dens, method = "ksmooth", smoothness = 0.2, n = 50L)

l3.smth.dens2 <- smooth(l3.smth.dens, method = "ksmooth", smoothness = 1, n = 50L)
  • 两个平滑多边形之一(红色,smoothness index = 1)与原始多边形(黑色)和 mrhellman 的多边形(蓝色)的一般比较
ggplot2::ggplot()+
  ggplot2::geom_sf(data = l3.smth.dens2, col = "red", fill = NA) + 
  ggplot2::geom_sf(data = l3.neg, col = "blue", fill = NA) +
  ggplot2::geom_sf(data = l2.base,  col = 'black', fill = NA) +
  ggplot2::geom_abline(intercept = 3, slope = 1, color="gray",linetype="dashed", size=1) 

  • 放大一个角:两个平滑的多边形(红色 - smoothness index = 1 - 和橙色 -smoothness index = 0.2)与原始多边形(黑色)和 mrhellman 的多边形(蓝色)的比较

请注意,对于 smoothness index = 0.2,内角的舍入比 mrhellman 解决方案的舍入更接近原始几何形状(如果您想要大致相同的舍入,请设置 smoothness index 至约 0.6)

ggplot2::ggplot()+
  ggplot2::geom_sf(data = l3.smth.dens2, col = "red", fill = NA) + 
  ggplot2::geom_sf(data = l3.smth.dens1, col = "orange", fill = NA) +
  ggplot2::geom_sf(data = l3.neg, col = "blue", fill = NA) +
  ggplot2::geom_sf(data = l2.base,  col = 'black', fill = NA) +
  ggplot2::geom_abline(intercept = 3, slope = 1, color="gray",linetype="dashed", size=1) +
  ggplot2::coord_sf(xlim = c(-1.55,-0.6), ylim = c(1.4,2.4))

  • 放大一个肢体:两个平滑的多边形(红色 - smoothness index = 1 - 和橙色 -smoothness index = 0.2)与原始多边形(黑色)和 mrhellman 的多边形(蓝色)的比较

请注意,对于 smoothness index = 0.2,尽管渲染比例过大

,但两端的圆角是微不足道的
ggplot2::ggplot()+
  ggplot2::geom_sf(data = l3.smth.dens2, col = "red", fill = NA) +
  ggplot2::geom_sf(data = l3.smth.dens1, col = "orange", fill = NA) +
  ggplot2::geom_sf(data = l3.neg, col = "blue", fill = NA) +
  ggplot2::geom_sf(data = l2.base,  col = 'black', fill = NA) +
  ggplot2::geom_abline(intercept = 3, slope = 1, color="gray",linetype="dashed", size=1) +
  ggplot2::coord_sf(xlim = c(-6,-4), ylim = c(2,4))

reprex package (v2.0.1)

于 2021-10-17 创建