Совокупная длина LINESTRING с использованием sf или terra

есть ли более эффективный способ измерения совокупной длины <LINESTRING> или <MULTILINESTRING>? Другими словами, мне нужно измерить расстояние между точками от начала до конца. Сейчас мне пришла в голову только идея разделить <LINESTRING> на отдельные сегменты и измерить длину каждого из них. Это работает, но занимает много времени из-за итеративного подхода. Существует ли какой-либо встроенный метод?

Ниже приведен репрекс, который я придумал. В качестве примера используется река Сена из spData. Несмотря на то, что в приведенном ниже примере используются пакеты sf и geos, я буду рад услышать о любых других пространственных пакетах, таких как terra, geos или даже rsgeo.

Ваше здоровье!

library(sf)
#> Linking to GEOS 3.10.2, GDAL 3.4.1, PROJ 8.2.1; sf_use_s2() is TRUE
library(geos)
library(spData)
#> To access larger datasets in this package, install the spDataLarge
#> package with: `install.packages('spDataLarge',
#> repos='https://nowosad.github.io/drat/', type='source')`

# Example
lines <- seine[2, ]

# My foo
cumulative_length <- 
  function(input) {
    
    # Save CRS
    crs <- sf::st_crs(input)
    
    # Retrive coordinates
    lines_coo <- 
      sf::st_coordinates(input)
    
    # Count number of segments of linestring
    n <- nrow(lines_coo) - 1
    
    # Pre-allocate a list 
    lines_geos <- 
      vector(mode = "list", length = n)
    
    # Construct linestrings
    for (i in 1:n) {
      lines_geos[[i]] <- 
        geos::geos_make_linestring(lines_coo[i:(i+1),1], 
                                   lines_coo[i:(i+1),2], 
                                   crs = crs)
    }
    
    # Measure cumulative segment length
    lines_order <- 
      sapply(lines_geos, geos::geos_length) |> 
      append(0, 0) |> 
      cumsum()
    
    return(lines_order)
    
  }

bench::mark(cumulative_length(lines))
#> # A tibble: 1 × 6
#>   expression                    min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>               <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 cumulative_length(lines)     13ms   13.7ms      72.9     244KB     109.

Created on 2024-03-23 with reprex v2.0.2

ОБНОВЛЯТЬ

Желаемый результат выглядит следующим образом: монотонно увеличивающийся числовой вектор длиной от 0 до st_length(lines):

cumulative_length(lines) |> 
  head()
#> [1]    0.000 1716.196 3290.379 4824.087 6745.759 7446.660

sf::st_length(seine[2, ]) возвращает 635526,7 [м]. Это то, что вам нужно?
L Tyrone 23.03.2024 11:59

@LTyrone Я глубоко извиняюсь за то, что вызвал путаницу, не предоставив желаемый результат. На самом деле я ищу числовой вектор сегментных линий, а не общую длину LINESTRING. См. обновленный вопрос

atsyplenkov 24.03.2024 02:27
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
165
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

При профилировании вашей функции большую часть времени использует geos_make_linestring. Итак, предполагая, что вы можете преобразовать свои данные в плоские координаты (UTM), вы можете пропустить это, вычислив расстояние между двумя точками напрямую:

library(sf)
library(geos)
library(spData)
# Example
input <- seine[2, ] |> 
  st_transform(23032)

# My foo
cumulative_length <- function(input) {
  
  # Save CRS
  crs <- sf::st_crs(input)
  
  # Retrive coordinates
  lines_coo <- 
    sf::st_coordinates(input)
  
  # Count number of segments of linestring
  n <- nrow(lines_coo) - 1
  
  # Pre-allocate a list 
  lines_geos <- 
    vector(mode = "list", length = n)
  
  # Construct linestrings
  for (i in 1:n) {
    lines_geos[[i]] <- 
      geos::geos_make_linestring(lines_coo[i:(i+1),1], 
                                 lines_coo[i:(i+1),2], 
                                 crs = crs)
  }
  
  # Measure cumulative segment length
  lines_order <- 
    sapply(lines_geos, geos::geos_length) |> 
    append(0, 0) |> 
    cumsum()
  
  return(lines_order)
}

cartesian_dist <- function(x1, y1, x2, y2) {
  # √[(x2 − x1)2 + (y2 − y1)2]
  xmax <- max(x1, x2)
  xmin <- min(x1, x2)
  ymax <- max(y1, y2)
  ymin <- min(y1, y2)
  sqrt((xmax - xmin)^2 + (ymax - ymin)^2)
}

cumulative_length2 <- function(input) {
    
    # Retrive coordinates
    lines_coo <- sf::st_coordinates(input)
    
    # Count number of segments of linestring
    n <- nrow(lines_coo) - 1
    
    # Pre-allocate a list 
    dists <- numeric(n)
    
    # Construct linestrings
    for (i in 1:n) {
      dists[[i]] <- cartesian_dist(lines_coo[i, 1], lines_coo[i, 2], lines_coo[i+1, 1], lines_coo[i+1, 2])
    }
    
    # Measure cumulative segment length
    c(0, cumsum(dists))
    
}

microbenchmark::microbenchmark(
  cumulative_length(input),
  cumulative_length2(input),
  times = 100
)
#> Unit: milliseconds
#>                       expr     min      lq      mean   median       uq     max
#>   cumulative_length(input) 22.6733 23.5284 27.012571 24.42025 28.93890 53.6292
#>  cumulative_length2(input)  2.5730  2.7048  3.484514  2.82825  2.98925 21.8944
#>  neval
#>    100
#>    100

Created on 2024-03-23 with reprex v2.1.0

Если вы не можете преобразовать координаты в плоские, вы также можете использовать проекцию WGS84 и использовать уравнение Хаверсина для расчета расстояния между точками вместо декартова расстояния:

haversine_dist <- function(lon1, lat1, lon2, lat2){
  R <- 6371e3 # metres
  phi1 <- lat1 * pi/180 # radians
  phi2 = lat2 * pi/180 # radians
  delta_phi = (lat2-lat1) * pi/180 # radians
  delta_lambda = (lon2-lon1) * pi/180
  
  a <- sin(delta_phi/2) * sin(delta_phi/2) + cos(phi1) * cos(phi2) * sin(delta_lambda/2) * sin(delta_lambda/2)
  c <- 2 * atan2(sqrt(a), sqrt(1-a))
  
  R * c
}

Спасибо, мне нравится! Очень элегантно. Кстати, как можно использовать профилирование функции R?

atsyplenkov 23.03.2024 21:58

В RStudio выберите вызов функции amd, затем в меню нажмите «Профилировать выбранные строки».

Josep Pueyo 23.03.2024 22:33

Вы можете использовать terra::perim, чтобы получить периметр многоугольников или длину линий.

library(terra)
v <- vect(system.file("ex/lux.shp", package = "terra"))
perim(v)
# [1] 117100.12  93477.28  84502.45  44919.14  85032.61  74708.05  57991.42  81203.93  74443.82  95564.74  80618.76 70810.67

Это работает как для плоских, так и для угловых (долгота/широта) координат.

Чтобы получить длину сегментов линий/многоугольников, вам необходимо сначала дезагрегировать их. Здесь показан первый многоугольник в v.

x <- disagg(v[1,], segments=TRUE)
p <- perim(x)
head(p)
#[1] 1383.51779  350.05402  580.19502   93.65138  333.32716  798.43439

sum(p)
#[1] 117100.1

И с вашими примерными данными вы получаете нужные цифры.

vect(lines) |> disagg(TRUE) |> perim() |> cumsum() |> head()
# [1] 1716.196 3290.379 4824.087 6745.759 7446.660 8303.283

Я глубоко извиняюсь за то, что вызвал путаницу, не предоставив желаемого результата. На самом деле я ищу числовой вектор сегментных линий, а не общую длину LINESTRING. См. обновленный вопрос

atsyplenkov 24.03.2024 02:27

Хорошо, я расширил свой ответ, чтобы охватить и это.

Robert Hijmans 24.03.2024 18:33

Спасибо всем за ваши быстрые ответы; Мне нравятся все из них. В каждом предыдущем ответе был пример огромного увеличения скорости по сравнению с исходным вопросом. Я немного поигрался с кодом и нашел, насколько мне известно, наиболее эффективный подход. Это может быть полезно для дальнейшего использования. Вот тест с функцией Rust, написанный с помощью пакета расширения. Он превосходит {terra} примерно на 10%. 18 раз и основание R примерно на . 9 раз. Полученные векторы идентичны (см. check = TRUE).

  ## RUST
  # Wrap code from Josep Pueyo answer into RUST function
  code <- r"(
    use extendr_api::prelude::*;

    #[extendr]
    fn cartesian_distance(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
      // Find max and min points
      let xmax = x1.max(x2);
      let xmin = x1.min(x2);
      let ymax = y1.max(y2);
      let ymin = y1.min(y2);

      // Calculate the distance between two points in 2D space
      let dx = xmax - xmin;
      let dy = ymax - ymin;
      (dx*dx + dy*dy).sqrt()
    }

    #[extendr]
    fn cartesian_distance_vector(x_vec: Vec<f64>, y_vec: Vec<f64>) -> Vec<f64> {

    // Pre-allocate result vector
    let mut cumulative_distances: Vec<f64> = Vec::with_capacity(x_vec.len());
    let n = x_vec.len() - 1;
    let mut cumulative_distance = 0.0;

    // Iterate over X-Y pairs
    for i in 0..n {

        let distance = cartesian_distance(x_vec[i], y_vec[i], x_vec[i+1], y_vec[i+1]);
        let distance = distance;

        // Accumulate cumulative distance
        cumulative_distance += distance;

        // Push cumulative distance to the result vector
        cumulative_distances.push(cumulative_distance);
    }

    cumulative_distances
}
)"
rextendr::rust_source(code = code)
#> ℹ build directory: 'C:/Users/TsyplenkovA/AppData/Local/Temp/RtmpquAnXb/file3d85de14e0f'
#> ✔ Writing 'C:/Users/TsyplenkovA/AppData/Local/Temp/RtmpquAnXb/file3d85de14e0f/target/extendr_wrappers.R'

## Base R
base_r <- function(X, Y){
  
  cartesian_dist <- function(x1, y1, x2, y2) {
    # √[(x2 − x1)2 + (y2 − y1)2]
    xmax <- max(x1, x2)
    xmin <- min(x1, x2)
    ymax <- max(y1, y2)
    ymin <- min(y1, y2)
    sqrt((xmax - xmin)^2 + (ymax - ymin)^2)
  }
  
  n <- length(X) - 1
  
  # Pre-allocate a list 
  dists <- numeric(n)
  
  # Construct linestrings
  for (i in 1:n) {
    dists[[i]] <- cartesian_dist(X[i], Y[i], X[i+1], Y[i+1])
  }
  
  # Measure cumulative segment length
  cumsum(dists)
  
}

## Terra
library(terra)
#> terra 1.7.71
library(sf) # sf package is only needed for reading seine
#> Linking to GEOS 3.11.2, GDAL 3.8.2, PROJ 9.3.1; sf_use_s2() is TRUE
lv <- vect(spData::seine[2, ])


## Benchmark
bench::mark(
  rust = cartesian_distance_vector(
    geom(lv)[, 3],
    geom(lv)[, 4]
  ),
  base_r = base_r(
    geom(lv)[, 3],
    geom(lv)[, 4]
  ),
  terra = {
    lv |>
      disagg(TRUE) |>
      perim() |>
      cumsum()
  },
  check = T,
  relative = T
)
#> # A tibble: 3 × 6
#>   expression   min median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <dbl>  <dbl>     <dbl>     <dbl>    <dbl>
#> 1 rust        1      1        16.9       10.1     4.71
#> 2 base_r      9.32   8.59      2.15      49.7     5.11
#> 3 terra      20.0   18.5       1          1       1

Created on 2024-03-26 with reprex v2.1.0

Версия Rcpp даже быстрее, чем версия Rust:

library(Rcpp)
cppFunction('NumericVector cartesian_distance_cpp(NumericVector x_vec, NumericVector y_vec) {
  int n = x_vec.size() - 1;
  NumericVector cumulative_distances(n);
  double cumulative_distance = 0.0;

  for (int i = 0; i < n; i++) {
    double distance = sqrt(pow(x_vec[i] - x_vec[i+1], 2) + pow(y_vec[i] - y_vec[i+1], 2));
    cumulative_distance += distance;
    cumulative_distances[i] = cumulative_distance;
  }
  return cumulative_distances;
}')

На моей машине Rust в 1,6 раза медленнее, чем Rcpp... но учтите, что {terra} выигрывает с точки зрения памяти, поскольку требует в 7 раз меньше памяти, чем Rust, Rcpp или base.

Другие вопросы по теме