homer3018
homer3018

Reputation: 329

Make a polygon from lines, circles and arcs using the sf package with R

So I need to make a polygon with R using the sf package, and all I have for that is a relatively simple dataframe where I can see points that make up lines, arcs (some parts of the polygons edge are arcs and not straitgh lines), and then circles (these are inside the polygon and are holes in the surface).

The polygon edges are lines or arcs, and they should connect to one another so that I don't have to use st_convex_hull or something like that. Full circles are inside the polygon and will be holes in the polygon at the end.

I'm new to using sf but I've figured out how to proceed for lines and circles, though there might be a better way.

I've created dummy data with 3 elements, one of each, and how I'm building the different geometries. I'm actually stuck with the arc part. I'm quite confident this should be easy enough, maybe using circularstring type but not sure if this is the best way. Obviously there won't be a final polygon with these dummy data but hopefully it gets the idea across.

Here's the dummy data and my code.

library(tidyverse)
#> Warning: package 'tidyverse' was built under R version 4.0.3
library(sf)
#> Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 6.3.1

(sample <- data.frame(number = 1,
                     name = c("Arc", "Circle", "Line"),
                     area = c(0.46, 330, NA),
                     start_angle = c(134, NA, NA),
                     angle_total = c(17, NA, NA),
                     center_x = c(974, 377, NA),
                     center_y = c(7299, 7250, NA),
                     center_z = c(0, 0, NA),
                     length = c(4.27, NA, 15),
                     radius = c(14, 10.2, NA),
                     angle = c(NA, NA, 270),
                     delta_x = c(NA, NA, 0),
                     delta_y = c(NA, NA, -15),
                     delta_z = c(NA, NA, 0),
                     start_x = c(NA, NA, 18.2),
                     start_y = c(NA, NA, 7000),
                     start_z = c(NA, NA, 0),
                     end_x = c(NA, NA, 18.2),
                     end_y = c(NA, NA, 146),
                     end_z = c(NA, NA, 0)) %>% 
  as_tibble())
#> # A tibble: 3 x 20
#>   number name    area start_angle angle_total center_x center_y center_z length
#>    <dbl> <chr>  <dbl>       <dbl>       <dbl>    <dbl>    <dbl>    <dbl>  <dbl>
#> 1      1 Arc     0.46         134          17      974     7299        0   4.27
#> 2      1 Circ~ 330             NA          NA      377     7250        0  NA   
#> 3      1 Line   NA             NA          NA       NA       NA       NA  15   
#> # ... with 11 more variables: radius <dbl>, angle <dbl>, delta_x <dbl>,
#> #   delta_y <dbl>, delta_z <dbl>, start_x <dbl>, start_y <dbl>, start_z <dbl>,
#> #   end_x <dbl>, end_y <dbl>, end_z <dbl>

line_start <- sample %>% 
  filter(name == "Line") %>% 
  select(start_x, start_y) %>% 
  rename(X = start_x, Y = start_y) %>% 
  mutate(ID = 1:nrow(.))

line_end <- sample %>% 
  filter(name == "Line") %>% 
  select(end_x, end_y) %>% 
  rename(X = end_x, Y = end_y) %>% 
  mutate(ID = 1:nrow(.))

(lines <- line_start %>% 
  bind_rows(line_end) %>%
  st_as_sf(coords = c("X", "Y")) %>%
  group_by(ID) %>%
  summarise(do_union = FALSE) %>%
  st_cast("LINESTRING"))
#> `summarise()` ungrouping output (override with `.groups` argument)
#> Simple feature collection with 1 feature and 1 field
#> geometry type:  LINESTRING
#> dimension:      XY
#> bbox:           xmin: 18.2 ymin: 146 xmax: 18.2 ymax: 7000
#> CRS:            NA
#> # A tibble: 1 x 2
#>      ID              geometry
#>   <int>          <LINESTRING>
#> 1     1 (18.2 7000, 18.2 146)

# ggplot() +
#   geom_sf(data = lines) +
#   coord_sf(xlim = c(10, 20))

circle_centers <- sample %>% 
  filter(name == "Circle") %>% 
  select(center_x, center_y) %>% 
  rename(X = center_x, Y = center_y) %>% 
  mutate(ID = 1:nrow(.))

circle_radii <- sample %>% 
  filter(name == "Circle") %>% 
  select(radius) %>% 
  rename(R = radius) %>% 
  mutate(ID = 1:nrow(.))

(circle <- circle_centers %>% 
  st_as_sf(coords = c("X", "Y")) %>%
  st_buffer(circle_radii$R))
#> Simple feature collection with 1 feature and 1 field
#> geometry type:  POLYGON
#> dimension:      XY
#> bbox:           xmin: 366.8 ymin: 7239.8 xmax: 387.2 ymax: 7260.2
#> CRS:            NA
#> # A tibble: 1 x 2
#>      ID                                                                 geometry
#> * <int>                                                                <POLYGON>
#> 1     1 ((387.2 7250, 387.186 7249.466, 387.1441 7248.934, 387.0744 7248.404, 3~

# ggplot() +
#   geom_sf(data = circle)

# ggplot() +
#   geom_sf(data = rbind(lines, circle)) +
#   coord_sf(xlim = c(-500, 1000))

Created on 2020-12-03 by the reprex package (v0.3.0)

The only steps that are missing would be to combine all these geometries to actually have the final polygon with holes inside, but that should be easy once I have all the separated geometries.

Also there might be better ways for handling the lines and circles part. Again I'm just starting out with sf so please feel free to teach me more efficient ways if you know one.

Thanks ahead of time for helping out !

Upvotes: 2

Views: 804

Answers (1)

homer3018
homer3018

Reputation: 329

So, let's start from the beginning, and how to make circularstring from the sample data I have (that's autoCAD export data btw).

arcs <- sample %>% 
    filter(name == "Arc") %>% 
    mutate(ID = 1:nrow(.)) %>% 
    mutate(X_1 = center_x + radius * cos(start_angle * pi / 180),
           Y_1 = center_y + radius * sin(start_angle * pi / 180),
           X_2 = center_x + radius * cos((angle_total + start_angle) * pi / 180),
           Y_2 = center_y + radius * sin((angle_total + start_angle) * pi / 180),
           X_3 = center_x + radius * cos((angle_total/2 + start_angle) * pi / 180),
           Y_3 = center_y + radius * sin((angle_total/2 + start_angle) * pi / 180))

This is doing most of the heavy lifting by having the center of the arc (in yellow), the extremities (green and red) and the midpoint on that arc (in blue) :

ggplot() +
  geom_point(aes(x = arcs$center_x, y = arcs$center_y), col = "yellow", alpha = 0.5) +
  geom_point(aes(x = arcs$X_1, y = arcs$Y_1), col = "red", alpha = 0.5) +
  geom_point(aes(x = arcs$X_2, y = arcs$Y_2), col = "green", alpha = 0.5) +
  geom_point(aes(x = arcs$X_3, y = arcs$Y_3), col = "blue", alpha = 0.5)

From there it is pretty strategy forward to construct your circularstring:

arcs_sf <- arcs %>% 
  mutate(CIRCULARSTRING = paste0("CIRCULARSTRING(", X_1, " ", Y_1, ", ", X_3, " ", Y_3, ", ", X_2, " ", Y_2, ")")) %>% 
  pull(CIRCULARSTRING) %>% 
  as.list() %>% 
  st_as_sfc() %>% 
  st_cast("LINESTRING") %>% 
  st_as_sf()

Plot to check everything is in good shape :

ggplot() +
  geom_point(aes(x = arcs$center_x, y = arcs$center_y), col = "yellow", alpha = 0.5) +
  geom_point(aes(x = arcs$X_1, y = arcs$Y_1), col = "red", alpha = 0.5) +
  geom_point(aes(x = arcs$X_2, y = arcs$Y_2), col = "green", alpha = 0.5) +
  geom_point(aes(x = arcs$X_3, y = arcs$Y_3), col = "blue", alpha = 0.5) +
    geom_sf(data = arcs_sf)

Now the tricky part is to end up with a polygon because often times, an arc extremity for instance that should be joining with a line, these 2 points (extremities) that should be at the exact same coordinate are actually not. So the shape is not close and st_polygon can't do anything. Since the polygon is made if arcs and lines (circles are holes inside this shape), you can play around with st_segmentize() to add points along lines and then with concaveman::concaveman(concavity = 0.1) where concavity is a guesstimation that you'd have to find out for your own use. I can't remember all the details but concaveman() is only considering points to construct a polygon, and not lines or arcs, hence the need of adding points prior to calling it.

Hopefully this is sheding some light and can be useful to someone.

Upvotes: 1

Related Questions