Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I split/divide polyline shapefiles into equally-length-smaller segments?

I have a polyline shapefile, which represents part of an urban road network. My shapefile contains several line/street segments (in my case 58). By using R-cran, I would like to further divide the polyline segments into smaller parts having equal length (e.g. 10 m).

To provide an idea:

When I import my polyline shapefile into R, and I create a dataframe, it looks like:

# Import the polyline shapefile into a SpatialLinesDataFrame object:
# library(sp)
sp.roads <- readOGR(dsn="/Users/mgv/Documents/project",layer="roads_sr_corr") 

# Create a dataframe from the SpatialLinesDataFrame 
# library(stplanr)    
linedf <- line2df(sp.roads)
str(linedf)
> str(linedf)
'data.frame':   58 obs. of  4 variables:
 $ fx: num  13.39991 13.40138 13.40606 13.40232 13.40177 ...
 $ fy: num  42.35066 42.35412 42.35599 42.34514 42.34534 ...
 $ tx: num  13.40150 13.40119 13.40591 13.40246 13.40182 ...
 $ ty: num  42.35026 42.35386 42.35602 42.34530 42.34525 ...

Where (fx, fy, tx, ty) are respectively the longitudes and latitudes of the points (x,y)_from, and (x,y)_to, delimiting each segment (here five).

The idea is to obtain a much denser polyline shapefile, which I can use for spatial analyses as a "sort of grid" to associate geo-referenced data points collected along the roads to each segment.

Many thanks for your help, and any suggestion to tackle this issue.

like image 839
MGV Avatar asked Feb 06 '23 11:02

MGV


2 Answers

The solution presented by Duccio resets the spacing on every new segment. I adapted the script to a version that does a running spacing. The difference is visualized in the images below.

Splitting as done in the approach by Duccio

The desired spacing (represented by length of green bar) always begins at each segment. Residual distances from the previous segments are not transferred to the next segment. Thus, the resulting spacing is not constant over the full line.

Splitting as done in my approach

The desired spacing is transferred arround corners. This approach garantuees that the points have always the desired spacing with regard to the original line. However, neither here is the spacing of the resulting points to each other constant. Furthermore, this approach produces a line shorter than the original line.

This effect decreases with a higher resolution.

Or can be prevented by setting the optional parameter "add_original_points" to TRUE.

Note: the "right algorithm to use" depends on your application. Disclaimer: my version is entirely based on the solution by Duccio A and I give him full credit.

My approach does not use Line objects (sp package) but works entirely on data frames. Thus, I would not want to fork the original github repos.

The full code is:

    resample_polyline  = function(polyline, interval_length = 20, add_original_points = TRUE, add_final_point = FALSE) {

  # The function splits a polyline into segments of a given length.
  # polyline: a spatial polyline data frame
  # interval_length: the length of the segments to split the lines into, in units of the polyline coordinates
  # add_original_points: whether or not the original points of the polyline should be added to the resulting line
  #                      if set FALSE, the resulting line will be shorter
  # add_final_point: whether or not the final point of the polyline should be added to the resulting line

  # transform input polyline
  linedf = data.frame(
    x  = polyline$x[1:nrow(polyline)-1],
    y  = polyline$y[1:nrow(polyline)-1],
    x2 = polyline$x[2:nrow(polyline)],
    y2 = polyline$y[2:nrow(polyline)]
  )

  # prepare output
  df = data.frame(
    x  = numeric(),
    y  = numeric()
  )

  residual_seg_length = 0
  for (i in 1:nrow(linedf)) {

    # for each line of the dataframe calculate segment length   
    v_seg      = linedf[i, ]
    seg_length = sqrt((v_seg$x - v_seg$x2) ^ 2 + (v_seg$y - v_seg$y2) ^ 2) 

    # create a vector of direction for the segment 
    v = c(v_seg$x2 - v_seg$x, v_seg$y2 - v_seg$y)

    # unit length
    u = c(v[1]  /  sqrt(v[1]  ^  2 + v[2]  ^  2), v[2]  /  sqrt(v[1]  ^  2 + v[2]  ^ 2))

    # calculate number of segment the segment is split into
    num_seg = floor((seg_length - residual_seg_length)  /  interval_length)

    # skip if next vertex is before interval_length
    if(num_seg >= 0) {      

      # add interpolated segments
      for (i in 0:(num_seg)) {
        df[nrow(df) + 1,] = c(
          v_seg$x  +  u[1] * residual_seg_length  +  u[1]  *  interval_length  *  i ,
          v_seg$y  +  u[2] * residual_seg_length  +  u[2]  *  interval_length  *  i
        )
      }

      # add original point (optional)
      if(add_original_points){
        df[nrow(df) + 1,] = c(
          v_seg$x2,
          v_seg$y2
        )
      }

    } else {

      # add original point (optional)
      if(add_original_points){
        df[nrow(df) + 1,] = c(
          v_seg$x2,
          v_seg$y2
        )
      }

      residual_seg_length = residual_seg_length - seg_length
      next()

    }

    # calculate residual segment length
    residual_seg_length = interval_length - ((seg_length - residual_seg_length) - (num_seg  *  interval_length))

  }

  # add final point (optional)
  if(add_final_point){
    df = rbind(df, data.frame(
      x = tail(polyline$x, n=1),
      y = tail(polyline$y, n=1)
    ))
  }

  return(df)

}

Test it with

polyline = data.frame(
  x = c(-5,1,5,7,8,12,14,16,17,13), # x
  y = c(0,11,3,8,2,15,9,13,15,23)   # y
)

plot(polyline$x, polyline$y, type="l", asp=1, lwd=1)
points(polyline$x, polyline$y, pch=4, cex=4, col="gray")

polyline2 = resample_polyline(polyline, interval_length = 5, add_final_point = FALSE, add_original_points = TRUE)
lines(polyline2$x,  polyline2$y, col="red", lty=4, lwd=3)
points(polyline2$x, polyline2$y, pch=19)

legend("topleft",
  c("original points", "added points", "resulting line"),
  pch = c(4, 19, NA),
  lty = c(NA, NA, 2),
  pt.cex = c(4,1,1),
  col = c("gray", "black", "red")
  )
like image 21
agoldev Avatar answered Feb 09 '23 01:02

agoldev


The following function divides each line of the spatial line object into segments of split_length length plus the leftover segment. It uses the vector notation of a segment, creating a vector u of unit length and direction of the line to be split that can be multiplied to create a longer line composed of many segments (here and here the references I have used).

SplitLines = function(spatial_line,
                      split_length = 20,
                      return.dataframe = F,
                      plot.results = F) {
  # The function splits each line of the spatial line object into segments of a given length
  # spatial_line: a spatial line object
  # split_length: the length of the segments to split the lines into, in units of the SpatialLine object
  # return.dataframe: if true it returns the segments in the form of a dataframe, otherwise in a SpatialLine object
  # plot.results: 
  require(sp)
  #### Define support functions ####
  # SpatialLines2df extracts start and end point coordinates of each segment of a SpatialLine object
  # spatial_line: an object class SpatialLinesDataFrame of the package sp
  SpatialLines2df = function(spatial_line) {
    df = data.frame(
      id = character(),
      mline_id = character(),
      segment_id = character(),
      fx = numeric(),
      fy = numeric(),
      tx = numeric(),
      ty = numeric(),
      stringsAsFactors = FALSE
    )
    for (i in 1:length(spatial_line)) {
      coords = spatial_line@lines[[i]]@Lines[[1]]@coords # For each line takes the coords of the vertex
      row_nums = 1:(nrow(coords) - 1)
      mline_id = formatC(i, width = 9, flag = '0') # Creates id for the line
      segment_id = formatC(row_nums, width = 3, flag = '0') # Creates id for each single segment belonging to the line
      id = paste0(mline_id, '_', segment_id) # Creates a composite id
      for (j in row_nums) {
        # For each segment stores ids and coordinates
        df[nrow(df) + 1, ] = c(id[j],
                               mline_id,
                               segment_id[j],
                               coords[j, 1],
                               coords[j, 2],
                               coords[j + 1, 1],
                               coords[j + 1, 2])
      }
    }
    row.names(df) = NULL
    df$fx = as.numeric(df$fx)
    df$fy = as.numeric(df$fy)
    df$tx = as.numeric(df$tx)
    df$ty = as.numeric(df$ty)
    return(df)
  }
  
  # linedf2SpatialLines converts a dataframe of IDs and coordinates into a spatial line
  # linedf: a data.frame with columns as:
  #         id = generic ids of the lines,
  #         fx = coordinates x of the first point of the line
  #         fy = coordinates y of the first point of the line
  #         tx = coordinates x of the second point of the line
  #         tx = coordinates y of the second point of the line
  
  require(sp)
  linedf2SpatialLines = function(linedf) {
    sl = list()
    for (i in 1:nrow(linedf)) {
      c1 = cbind(rbind(linedf$fx[i], linedf$tx[i]),
                 rbind(linedf$fy[i], linedf$ty[i]))
      l1 = Line(c1)
      sl[[i]] = Lines(list(l1), ID = linedf$id[i])
    }
    SL = SpatialLines(sl)
    return(SL)
  }
  
  
  #### Split the lines ####
  # Convert the input SpatialLine object into a dataframe and create an empty output dataframe
  linedf = SpatialLines2df(spatial_line)
  df = data.frame(
    id = character(),
    fx = numeric(),
    fy = numeric(),
    tx = numeric(),
    ty = numeric(),
    stringsAsFactors = FALSE
  )
  
  
  for (i in 1:nrow(linedf)) {
    # For each line of the dataframe, corresponding to a single line of the spatial object
    # skips if length is less then split_length
    v_seg = linedf[i, ]
    seg_length = sqrt((v_seg$fx - v_seg$tx) ^ 2 + (v_seg$fy - v_seg$ty) ^
                        2) # segment length
    if (seg_length <= split_length) {
      df[nrow(df) + 1,] = c(paste0(v_seg$id, '_', '0000'),
                            v_seg$fx,
                            v_seg$fy,
                            v_seg$tx,
                            v_seg$ty)
      next()
    }
    # Create a vector of direction as the line and unit length
    # vector v corresponding to the line
    v = c(v_seg$tx  -  v_seg$fx,
          v_seg$ty  -  v_seg$fy)
    # vector of direction v and unit length
    u = c(v[1]  /  sqrt(v[1]  ^  2 + v[2]  ^  2), v[2]  /  sqrt(v[1]  ^  2 + v[2]  ^ 2))
    # Calculates how many segment the line is split into and the leftover
    num_seg = floor(seg_length  /  split_length)
    seg_left = seg_length - (num_seg  *  split_length)
    
    # Add to the output dataframe each segment plus the leftover
    for (i in 0:(num_seg  -  1)) {
      # Add num_seg segments
      df[nrow(df)  +  1,] = c(
        paste0(v_seg$id, '_', formatC(i, width = 4, flag = '0')),
        v_seg$fx + u[1]  *  split_length  *  i,
        v_seg$fy + u[2]  *  split_length  *  i,
        v_seg$fx + u[1]  *  split_length  *  (i  +  1),
        v_seg$fy + u[2]  *  split_length  *  (i  +  1)
      )
    }
    df[nrow(df) + 1,] = c(
      paste0(v_seg$id, '_', formatC(
        num_seg, width = 4, flag = '0'
      )),
      # Add leftover segment
      v_seg$fx + u[1] * split_length * num_seg,
      v_seg$fy + u[2] * split_length * num_seg,
      v_seg$tx,
      v_seg$ty
    )
    
  }
  
  #### Visualise the results to check ####
  if (plot.results) {
    plot(spatial_line)
    coords = cbind(as.numeric(df$fx), as.numeric(df$fy))
    coords = rbind(coords, as.numeric(df$tx[nrow(df)]), as.numeric(df$ty)[nrow(df)])
    sp_points = SpatialPoints(coords)
    plot(sp_points, col = 'red', add = T)
  }
  
  #### Output ####
  df$fx = as.numeric(df$fx)
  df$fy = as.numeric(df$fy)
  df$tx = as.numeric(df$tx)
  df$ty = as.numeric(df$ty)
  if (return.dataframe) {
    return(df)
  } # Return a dataframe
  sl = linedf2SpatialLines(df)
  return(sl) # Return a SpatialLine object
}

You can test the function with the following:

Sl = SpatialLines(list(Lines(list(Line(cbind(c(1,2,3, 4),c(3,2,2,4)))), ID="a")))
plot(Sl)
Sl_split = SplitLines(Sl, split_length = 0.1, return.dataframe = FALSE, plot.results = TRUE)

I am sure that this function can be written in a more concise and efficient way. I have created a package in a git repository, in case anyone would like to contribute.

like image 55
Duccio A Avatar answered Feb 09 '23 01:02

Duccio A