Search code examples
rfunctionrbind

Speed up R function that combines linestrings into a single linestring


I have a function that i run on a large dataset that gets incredibly slow as rows are being incorporated. Is there a better way to run this function to speed up processing time? The function combines single line strings that are within 200' of each other into a longer linestring. I am also open to other methods not involving lapply.

library(sf)
library(tidyverse)
library(igraph)
library(nngeo)

stick_f=structure(list(DSU_ID = c(30143L, 30143L, 30143L, 30143L, 30143L, 
30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 
30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 
30143L, 30143L, 30143L, 30143L, 30143L), reservoir = c("WFMP A", 
"WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", 
"WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", 
"WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", 
"WFMP A", "WFMP A", "WFMP A", "WFMP A"), geometry = structure(list(
    structure(list(structure(c(1510332.04059501, 1511567.79520768, 
    849661.246296354, 844330.919899467), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1511533.50828716, 
    1511567.79520425, 844478.788270421, 844330.919898719), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1510332.04059501, 1510383.88784789, 849661.246296354, 
        849437.568787994), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1512938.64923346, 
    1513028.26383056, 839587.752188875, 839279.404999561), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1511560.82110934, 1513028.26383643, 844329.401308649, 
        839279.405001099), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1512036.9387796, 
    1513286.73238665, 850096.150779249, 844705.211814439), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1512036.9387796, 1512062.05174295, 850096.150779249, 
        849987.80810267), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1514694.78480917, 
    1514730.71451464, 839848.195042488, 839724.566787957), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1513283.63032551, 1514730.71451912, 844704.53635614, 
        839724.566789123), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1512889.37305887, 
    1514146.19959474, 850313.65971973, 844892.356013518), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1514145.0335769, 1515581.93862757, 844892.102120277, 
        839947.145827345), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1511184.49463734, 
    1512427.26425722, 849878.679583018, 844518.066442754), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1512402.53803832, 1512427.2642537, 844624.703079266, 
        844518.066441988), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1511184.49463734, 
    1511223.02826579, 849878.679583018, 849712.438260175), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1513816.7304309, 1513879.48958261, 839717.929806354, 
        839501.986511818), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1512422.22616923, 
    1513879.48958837, 844516.969418583, 839501.986513319), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1508630.11630082, 1509848.85435316, 849213.495159891, 
        843956.62330211), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1509795.42699765, 
    1509848.8543523, 844187.036127354, 843956.62330191), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1508630.11582111, 1508713.98473253, 849213.495030371, 
        848851.675174753), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1511181.66023315, 
    1511316.8140365, 839330.227183531, 838865.191067158), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1509838.00828373, 1511316.81536509, 843954.261575673, 
        838865.191385775), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1509479.58361439, 
    1510708.32523925, 849443.820894886, 844143.772185577), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1510664.47714004, 1510708.32523671, 844332.873984322, 
        844143.772185024), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1509479.58361439, 
    1509548.92420749, 849443.820894886, 849144.676051076), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
        structure(c(1512060.205399, 1512173.05327267, 839458.817683183, 
        839070.530367622), dim = c(2L, 2L))), class = c("XY", 
    "MULTILINESTRING", "sfg")), structure(list(structure(c(1510699.41514708, 
    1512173.05386546, 844141.832027335, 839070.530509783), dim = c(2L, 
    2L))), class = c("XY", "MULTILINESTRING", "sfg"))), class = c("sfc_MULTILINESTRING", 
"sfc"), precision = 0, bbox = structure(c(xmin = 1508630.11582111, 
ymin = 838865.191067158, xmax = 1515581.93862757, ymax = 850313.65971973
), class = "bbox"), crs = structure(list(input = "NAD27 / Texas Central", 
    wkt = "PROJCRS[\"NAD27 / Texas Central\",\n    BASEGEOGCRS[\"NAD27\",\n        DATUM[\"North American Datum 1927\",\n            ELLIPSOID[\"Clarke 1866\",6378206.4,294.978698213898,\n                LENGTHUNIT[\"metre\",1]]],\n        PRIMEM[\"Greenwich\",0,\n            ANGLEUNIT[\"degree\",0.0174532925199433]],\n        ID[\"EPSG\",4267]],\n    CONVERSION[\"Texas CS27 Central zone\",\n        METHOD[\"Lambert Conic Conformal (2SP)\",\n            ID[\"EPSG\",9802]],\n        PARAMETER[\"Latitude of false origin\",29.6666666666667,\n            ANGLEUNIT[\"degree\",0.0174532925199433],\n            ID[\"EPSG\",8821]],\n        PARAMETER[\"Longitude of false origin\",-100.333333333333,\n            ANGLEUNIT[\"degree\",0.0174532925199433],\n            ID[\"EPSG\",8822]],\n        PARAMETER[\"Latitude of 1st standard parallel\",30.1166666666667,\n            ANGLEUNIT[\"degree\",0.0174532925199433],\n            ID[\"EPSG\",8823]],\n        PARAMETER[\"Latitude of 2nd standard parallel\",31.8833333333333,\n            ANGLEUNIT[\"degree\",0.0174532925199433],\n            ID[\"EPSG\",8824]],\n        PARAMETER[\"Easting at false origin\",2000000,\n            LENGTHUNIT[\"US survey foot\",0.304800609601219],\n            ID[\"EPSG\",8826]],\n        PARAMETER[\"Northing at false origin\",0,\n            LENGTHUNIT[\"US survey foot\",0.304800609601219],\n            ID[\"EPSG\",8827]]],\n    CS[Cartesian,2],\n        AXIS[\"easting (X)\",east,\n            ORDER[1],\n            LENGTHUNIT[\"US survey foot\",0.304800609601219]],\n        AXIS[\"northing (Y)\",north,\n            ORDER[2],\n            LENGTHUNIT[\"US survey foot\",0.304800609601219]],\n    USAGE[\n        SCOPE[\"Engineering survey, topographic mapping.\"],\n        AREA[\"United States (USA) - Texas - counties of Anderson; Angelina; Bastrop; Bell; Blanco; Bosque; Brazos; Brown; Burleson; Burnet; Cherokee; Coke; Coleman; Comanche; Concho; Coryell; Crane; Crockett; Culberson; Ector; El Paso; Falls; Freestone; Gillespie; Glasscock; Grimes; Hamilton; Hardin; Houston; Hudspeth; Irion; Jasper; Jeff Davis; Kimble; Lampasas; Lee; Leon; Liberty; Limestone; Llano; Loving; Madison; Mason; McCulloch; McLennan; Menard; Midland; Milam; Mills; Montgomery; Nacogdoches; Newton; Orange; Pecos; Polk; Reagan; Reeves; Robertson; Runnels; Sabine; San Augustine; San Jacinto; San Saba; Schleicher; Shelby; Sterling; Sutton; Tom Green; Travis; Trinity; Tyler; Upton; Walker; Ward; Washington; Williamson; Winkler.\"],\n        BBOX[29.78,-106.66,32.27,-93.5]],\n    ID[\"EPSG\",32039]]"), class = "crs"), n_empty = 0L), 
    FilterId = c("30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A")), row.names = c(NA, 
26L), sf_column = "geometry", agr = structure(c(DSU_ID = NA_integer_, 
reservoir = NA_integer_, FilterId = NA_integer_), levels = c("constant", 
"aggregate", "identity"), class = "factor"), class = c("sf", 
"tbl_df", "tbl", "data.frame"))


vec_filter<- st_drop_geometry(stick_f)%>%mutate(FilterId=paste(DSU_ID,reservoir,sep="_"))%>%distinct(FilterId)
vec_filter<-vec_filter$FilterId


Here is the function that i run on the data to populate a new table.

combined_basin_lines<-data.frame()

#i=vec_filter[761]

touch_df_fctn<- function(i){
  df_filter<-stick_f%>%filter(FilterId==i)
  
  (my_idx_touches <- st_intersects(st_buffer(df_filter,dist=200,endCapStyle = 'ROUND')))
  (my_igraph <- graph_from_adj_list(my_idx_touches))
  (my_components <- components(my_igraph)$membership)
  
  sf_ml3 <- df_filter %>% 
    group_by(section = as.character({{my_components}})) %>% 
    summarise()%>%
    mutate(row_id=row_number())
  
  pt2 <- st_cast(sf_ml3, "MULTIPOINT")%>%group_by(section)%>%mutate(id=row_number())%>%
    st_cast('POINT')
  pt2 <-sfc_as_cols(pt2)%>%select(-geometry)
  
  pt2<-pt2%>%group_by(row_id)%>%filter(y==max(y)|y==min(y))
  
  pt2 <-st_as_sf(pt2,coords=c("x","y")) %>% group_by(row_id) %>% summarize(m = mean(row_number())) %>% st_cast("LINESTRING")
  pt_touch<-pt2
  
  mround <-
    function(number, multiple)
      multiple * round(number / multiple)
  
  
  pt=st_segments(pt_touch)
  pt<-st_as_sf(pt)%>%mutate(length=as.numeric(st_length(.)),id=row_number())
  
  lookup <- c(geometry = "result")
  
  pt<-pt%>%rename(any_of(lookup))
  
  pt<-pt%>%mutate(reservoir=unique(df_filter$reservoir))
  
  combined_basin_lines<<-rbind(combined_basin_lines,pt)
  
  print(i)
  
}

sfc_as_cols <- function(x, geometry, names = c("x", "y")) {
  if (missing(geometry)) {
    geometry <- sf::st_geometry(x)
  } else {
    geometry <- rlang::eval_tidy(enquo(geometry), x)
  }
  stopifnot(inherits(x, "sf") &&
              inherits(geometry, "sfc_POINT"))
  ret <- sf::st_coordinates(geometry)
  ret <- tibble::as_tibble(ret)
  stopifnot(length(names) == ncol(ret))
  x <- x[, !names(x) %in% names]
  ret <- setNames(ret, names)
  dplyr::bind_cols(x, ret)
}


lapply(vec_filter,touch_df_fctn)  

Solution

  • Main issue here is the way lapply & rbind are used:

    combined_basin_lines <- data.frame()
    
    touch_df_fctn <- function(i){
      ...
      combined_basin_lines <<- rbind(combined_basin_lines, resulting_sf_object)
      print(i)
    }
    
    lapply(vec_filter, touch_df_fctn)  
    

    rbinding frames iteratively like this is super-expensive, every time rbind() gets called, it makes a new copy of the whole combined_basin_lines object.

    lapply() is a functional programming tool and called function should generally have no side effects -- stuff goes in, transformed stuff comes out and no messing with parent/global environment in the function. If this doesn't quite fit, it's probably not the best function to use with lapply().

    Regarding growing objects (here: rbind in a loop) and (any) use of <<- , you might want to check The R Inferno, cricles 2 & 6.


    To make this work with lapply() / purrr::map() and perhaps with dplyr::reframe(), let's start by checking what we actually deal with:

    library(sf)
    #> Linking to GEOS 3.12.1, GDAL 3.8.4, PROJ 9.3.1; sf_use_s2() is TRUE
    library(tidyverse)
    library(igraph, warn.conflicts = FALSE)
    library(nngeo)
    
    # input sf
    stick_f
    #> Simple feature collection with 26 features and 3 fields
    #> Geometry type: MULTILINESTRING
    #> Dimension:     XY
    #> Bounding box:  xmin: 1508630 ymin: 838865.2 xmax: 1515582 ymax: 850313.7
    #> Projected CRS: NAD27 / Texas Central
    #> # A tibble: 26 × 4
    #>    DSU_ID reservoir                               geometry FilterId    
    #>  *  <int> <chr>         <MULTILINESTRING [US_survey_foot]> <chr>       
    #>  1  30143 WFMP A    ((1510332 849661.2, 1511568 844330.9)) 30143_WFMP A
    #>  2  30143 WFMP A    ((1511534 844478.8, 1511568 844330.9)) 30143_WFMP A
    #>  3  30143 WFMP A    ((1510332 849661.2, 1510384 849437.6)) 30143_WFMP A
    #>  4  30143 WFMP A    ((1512939 839587.8, 1513028 839279.4)) 30143_WFMP A
    #>  5  30143 WFMP A    ((1511561 844329.4, 1513028 839279.4)) 30143_WFMP A
    #>  6  30143 WFMP A    ((1512037 850096.2, 1513287 844705.2)) 30143_WFMP A
    #>  7  30143 WFMP A    ((1512037 850096.2, 1512062 849987.8)) 30143_WFMP A
    #>  8  30143 WFMP A    ((1514695 839848.2, 1514731 839724.6)) 30143_WFMP A
    #>  9  30143 WFMP A    ((1513284 844704.5, 1514731 839724.6)) 30143_WFMP A
    #> 10  30143 WFMP A    ((1512889 850313.7, 1514146 844892.4)) 30143_WFMP A
    #> # ℹ 16 more rows
    
    # combined_basin_lines, result of included example:
    combined_basin_lines |> as_tibble() |> st_sf()
    #> Simple feature collection with 6 features and 5 fields
    #> Geometry type: LINESTRING
    #> Dimension:     XY
    #> Bounding box:  xmin: 1508630 ymin: 838865.2 xmax: 1515582 ymax: 850313.7
    #> Projected CRS: NAD27 / Texas Central
    #> # A tibble: 6 × 6
    #>   row_id     m length    id                             geometry reservoir
    #>    <int> <dbl>  <dbl> <int>        <LINESTRING [US_survey_foot]> <chr>    
    #> 1      1   2   10726.     1 (1510332 849661.2, 1513028 839279.4) WFMP A   
    #> 2      2   2   10716.     2 (1512037 850096.2, 1514731 839724.6) WFMP A   
    #> 3      3   1.5 10710.     3 (1512889 850313.7, 1515582 839947.1) WFMP A   
    #> 4      4   2   10721.     4   (1511184 849878.7, 1513879 839502) WFMP A   
    #> 5      5   1.5 10691.     5 (1508630 849213.5, 1511317 838865.2) WFMP A   
    #> 6      6   2   10717.     6 (1509480 849443.8, 1512173 839070.5) WFMP A
    
    set.seed(42)
    stick_f |> 
      mutate(section = LETTERS[sample(n())]) |>
      ggplot() +
      geom_sf(aes(color = section), linewidth = 1.5, alpha = .5) +
      geom_sf(data = st_cast(stick_f, "POINT"), shape = 4, size = 3, alpha = .8) +
      geom_sf(data = combined_basin_lines, color = "red") +
      geom_sf(data = st_cast(combined_basin_lines, "POINT"), shape = 1, size = 4, color = "red") +
      scale_color_viridis_d() +
      guides(color = guide_none()) +
      theme_minimal()
    

    So as far as I can tell, it is not really about combining lines but identifying 2 points (red circles) from each cluster and building new lines (red) from those.

    I used the same idea with slightly different approach:

    • cluster detection though igraph is super-neat, but let's refactor it to a function that we can use in e.g. group_by()
    • to pick 2 points from every cluster for our linestrings, let's use 2 shorter edges of st_minimum_rotated_rectangle().
    # sfc clustering with a buffer, 
    # return igraph component membership ids (row / vector idx)
    connected_comp <- function(sfc_, dist = 200){
      sfc_ |> 
        st_buffer(dist = dist, endCapStyle = 'ROUND') |> 
        st_intersects() |> 
        graph_from_adj_list(mode = "all") |> 
        simplify() |> 
        components() |> 
        pluck(membership) |> 
        # nngeo seems to have some issues with a membership class in tibble column
        as.numeric()
    }
    
    # cluster sfc, 2-point linestring from each cluster,
    # returns sf object with comp & geometry columns
    clust_lines <- function(sfc_, dist = 200){
      # (multi)lines to points with cluster/component id
      point_clust <- 
        st_sf(geometry = sfc_) |> 
        group_by(comp = connected_comp(geometry, dist = dist)) |> 
        st_cast("POINT", warn = FALSE) |> 
        distinct(geometry) |> 
        ungroup()
      
      # shorter edges of minimum rotated rectangles for each cluster
      clust_rect_end_sections <- 
        point_clust |>  
        group_by(comp) |> 
        summarise() |> 
        st_minimum_rotated_rectangle() |> 
        # note: nngeo::st_segments() drops c("tbl_df", "tbl") and returns 
        #       class c("sf", "data.frame") with "results" geometry column
        st_segments(progress = FALSE) |> 
        st_set_geometry("geometry") |> 
        slice_min(st_length(geometry), n = 2, by = comp)
      
      # lines from points that are closest to clust_rect_end_sections (2 points per cluster)
      point_clust |> 
        slice(st_nearest_feature(clust_rect_end_sections, geometry)) |> 
        group_by(comp) |> 
        summarise() |> 
        st_cast("LINESTRING") # |> 
    }
    

    We now have a function that takes a single object (sfc or sf) and returns a single transformed object (sf), no side effects. To use it with lapply() or purrr::.map(), we can start by creating a list of sf or sfc objects (e.g. split by FilterId) and expect a matching list of processed sf objects in return. Those can be further processed and/or combined into a single sf object.

    # split by FilterId, apply clust_lines, bind rows and restore FilterId column
    split(stick_f, ~FilterId) |> 
      map(clust_lines) |> 
      list_rbind(names_to = "FilterId") |> 
      as_tibble() |> 
      st_sf() 
    #> Simple feature collection with 6 features and 2 fields
    #> Geometry type: LINESTRING
    #> Dimension:     XY
    #> Bounding box:  xmin: 1508630 ymin: 838865.2 xmax: 1515582 ymax: 850313.7
    #> Projected CRS: NAD27 / Texas Central
    #> # A tibble: 6 × 3
    #>   FilterId      comp                             geometry
    #>   <chr>        <dbl>        <LINESTRING [US_survey_foot]>
    #> 1 30143_WFMP A     1 (1510332 849661.2, 1513028 839279.4)
    #> 2 30143_WFMP A     2 (1512037 850096.2, 1514731 839724.6)
    #> 3 30143_WFMP A     3 (1512889 850313.7, 1515582 839947.1)
    #> 4 30143_WFMP A     4   (1511184 849878.7, 1513879 839502)
    #> 5 30143_WFMP A     5 (1508630 849213.5, 1511317 838865.2)
    #> 6 30143_WFMP A     6 (1509480 849443.8, 1512173 839070.5)
    

    Or we can use it though dplyr::reframe() as each group could return an arbitrary number of rows, here we can group directly by DSU_ID & reservoir:

    stick_f |> 
      as_tibble() |> 
      reframe(clust_lines(geometry), .by = c(DSU_ID, reservoir)) |> 
      st_sf()
    #> Simple feature collection with 6 features and 3 fields
    #> Geometry type: LINESTRING
    #> Dimension:     XY
    #> Bounding box:  xmin: 1508630 ymin: 838865.2 xmax: 1515582 ymax: 850313.7
    #> Projected CRS: NAD27 / Texas Central
    #> # A tibble: 6 × 4
    #>   DSU_ID reservoir  comp                             geometry
    #>    <int> <chr>     <dbl>        <LINESTRING [US_survey_foot]>
    #> 1  30143 WFMP A        1 (1510332 849661.2, 1513028 839279.4)
    #> 2  30143 WFMP A        2 (1512037 850096.2, 1514731 839724.6)
    #> 3  30143 WFMP A        3 (1512889 850313.7, 1515582 839947.1)
    #> 4  30143 WFMP A        4   (1511184 849878.7, 1513879 839502)
    #> 5  30143 WFMP A        5 (1508630 849213.5, 1511317 838865.2)
    #> 6  30143 WFMP A        6 (1509480 849443.8, 1512173 839070.5)
    

    Example data (stick_f) & result from OP's code (combined_basin_lines):

    stick_f <- 
      structure(list(DSU_ID = c(30143L, 30143L, 30143L, 30143L, 30143L, 
    30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 
    30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 30143L, 
    30143L, 30143L, 30143L, 30143L, 30143L), reservoir = c("WFMP A", 
    "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", 
    "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", 
    "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A", 
    "WFMP A", "WFMP A", "WFMP A", "WFMP A"), geometry = structure(list(
        structure(list(structure(c(1510332.04059501, 1511567.79520768, 
        849661.246296354, 844330.919899467), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1511533.50828716, 
        1511567.79520425, 844478.788270421, 844330.919898719), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1510332.04059501, 1510383.88784789, 849661.246296354, 
            849437.568787994), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1512938.64923346, 
        1513028.26383056, 839587.752188875, 839279.404999561), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1511560.82110934, 1513028.26383643, 844329.401308649, 
            839279.405001099), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1512036.9387796, 
        1513286.73238665, 850096.150779249, 844705.211814439), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1512036.9387796, 1512062.05174295, 850096.150779249, 
            849987.80810267), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1514694.78480917, 
        1514730.71451464, 839848.195042488, 839724.566787957), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1513283.63032551, 1514730.71451912, 844704.53635614, 
            839724.566789123), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1512889.37305887, 
        1514146.19959474, 850313.65971973, 844892.356013518), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1514145.0335769, 1515581.93862757, 844892.102120277, 
            839947.145827345), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1511184.49463734, 
        1512427.26425722, 849878.679583018, 844518.066442754), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1512402.53803832, 1512427.2642537, 844624.703079266, 
            844518.066441988), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1511184.49463734, 
        1511223.02826579, 849878.679583018, 849712.438260175), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1513816.7304309, 1513879.48958261, 839717.929806354, 
            839501.986511818), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1512422.22616923, 
        1513879.48958837, 844516.969418583, 839501.986513319), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1508630.11630082, 1509848.85435316, 849213.495159891, 
            843956.62330211), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1509795.42699765, 
        1509848.8543523, 844187.036127354, 843956.62330191), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1508630.11582111, 1508713.98473253, 849213.495030371, 
            848851.675174753), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1511181.66023315, 
        1511316.8140365, 839330.227183531, 838865.191067158), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1509838.00828373, 1511316.81536509, 843954.261575673, 
            838865.191385775), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1509479.58361439, 
        1510708.32523925, 849443.820894886, 844143.772185577), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1510664.47714004, 1510708.32523671, 844332.873984322, 
            844143.772185024), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1509479.58361439, 
        1509548.92420749, 849443.820894886, 849144.676051076), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg")), structure(list(
            structure(c(1512060.205399, 1512173.05327267, 839458.817683183, 
            839070.530367622), dim = c(2L, 2L))), class = c("XY", 
        "MULTILINESTRING", "sfg")), structure(list(structure(c(1510699.41514708, 
        1512173.05386546, 844141.832027335, 839070.530509783), dim = c(2L, 
        2L))), class = c("XY", "MULTILINESTRING", "sfg"))), class = c("sfc_MULTILINESTRING", 
    "sfc"), precision = 0, bbox = structure(c(xmin = 1508630.11582111, 
    ymin = 838865.191067158, xmax = 1515581.93862757, ymax = 850313.65971973
    ), class = "bbox"), crs = structure(list(input = NA_character_, 
        wkt = NA_character_), class = "crs"), n_empty = 0L), FilterId = c("30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", "30143_WFMP A", 
    "30143_WFMP A")), row.names = c(NA, 26L), sf_column = "geometry", agr = structure(c(DSU_ID = NA_integer_, 
    reservoir = NA_integer_, FilterId = NA_integer_), levels = c("constant", 
    "aggregate", "identity"), class = "factor"), class = c("sf", 
    "tbl_df", "tbl", "data.frame")) |> 
      sf::st_set_crs(32039)
    
    combined_basin_lines <- 
      structure(list(row_id = 1:6, m = c(2, 2, 1.5, 2, 1.5, 2), length = c(10726.2411145618, 
    10715.6979333647, 10710.4864325931, 10720.9494378492, 10691.3863609254, 
    10717.2727483768), id = 1:6, geometry = structure(list(structure(c(1510332.04059501, 
    1513028.26383056, 849661.246296354, 839279.404999561), dim = c(2L, 
    2L), class = c("XY", "LINESTRING", "sfg")), structure(c(1512036.9387796, 
    1514730.71451464, 850096.150779249, 839724.566787957), dim = c(2L, 
    2L), class = c("XY", "LINESTRING", "sfg")), structure(c(1512889.37305887, 
    1515581.93862757, 850313.65971973, 839947.145827345), dim = c(2L, 
    2L), class = c("XY", "LINESTRING", "sfg")), structure(c(1511184.49463734, 
    1513879.48958261, 849878.679583018, 839501.986511818), dim = c(2L, 
    2L), class = c("XY", "LINESTRING", "sfg")), structure(c(1508630.11630082, 
    1511316.8140365, 849213.495159891, 838865.191067158), dim = c(2L, 
    2L), class = c("XY", "LINESTRING", "sfg")), structure(c(1509479.58361439, 
    1512173.05327267, 849443.820894886, 839070.530367622), dim = c(2L, 
    2L), class = c("XY", "LINESTRING", "sfg"))), class = c("sfc_LINESTRING", 
    "sfc"), precision = 0, bbox = structure(c(xmin = 1508630.11630082, 
    ymin = 838865.191067158, xmax = 1515581.93862757, ymax = 850313.65971973
    ), class = "bbox"), crs = structure(list(input = NA_character_, 
        wkt = NA_character_), class = "crs"), n_empty = 0L), reservoir = c("WFMP A", 
    "WFMP A", "WFMP A", "WFMP A", "WFMP A", "WFMP A")), row.names = c(NA, 
    6L), sf_column = "geometry", agr = structure(c(row_id = NA_integer_, 
    m = NA_integer_, length = NA_integer_, id = NA_integer_, reservoir = NA_integer_
    ), levels = c("constant", "aggregate", "identity"), class = "factor"), class = c("sf", 
    "data.frame")) |> 
      sf::st_set_crs(32039)
    

    Created on 2024-09-25 with reprex v2.1.1