Posicionamento automático de etiquetas para mapas GIS em R

9

Estou fazendo mapas GIS no R usando o sfpacote (e pacotes relacionados) para ler em shapefiles e ggplot2(e amigos) para plotagem. Isso funciona bem, mas não consigo encontrar (automaticamente / programaticamente) canais de etiqueta para recursos como rios e estradas. Esses recursos geralmente são cadeias de linhas, com formas irregulares. Veja a imagem em anexo, por exemplo, da wikimedia.

insira a descrição da imagem aqui

O ggrepelpacote funciona bem para rotular pontos de maneira automatizada, mas isso não faz muito sentido para outros recursos geográficos que não são pontos Lat / Long distintos.

Eu poderia imaginar isso colocando rótulos de texto individuais em cada recurso individualmente, mas estou procurando algo mais automatizado, se possível. Sei que essa automação não é um problema trivial, mas já foi resolvido antes (o ArcGIS aparentemente tem uma maneira de fazer isso com uma extensão chamada maplex, mas eu não tenho acesso ao software e gostaria de ficar em R se possível).

Alguém sabe de uma maneira de fazer isso?

MWE aqui:

#MWE Linestring labeling

library(tidyverse)
library(sf)
library(ggrepel)
set.seed(120)

#pick a county from the built-in North Carolina dataset
BuncombeCounty <- st_read(system.file("shapes/", package="maptools"), "sids") %>% 
  filter(NAME == "Buncombe") 

#pick 4 random points in that county
pts_sf <- data.frame(
  x = seq(-82.3, -82.7, by=-0.1) %>% 
    sample(4),
  y = seq(35.5, 35.7, by=0.05) %>% 
    sample(4),
  placenames = c("A", "B", "C", "D")
) %>% 
  st_as_sf(coords = c("x","y")) 

#link those points into a linestring
linestring_sf <- pts_sf %>% 
  st_coordinates() %>%
  st_linestring()
  st_cast("LINESTRING") 

#plot them with labels, using geom_text_repel() from the `ggrepel` package
ggplot() +
  geom_sf(data = BuncombeCounty) +
  geom_sf(data = linestring_sf) +
  geom_label_repel(data = pts_sf,
                  stat = "sf_coordinates",
                  aes(geometry = geometry,
                      label = placenames),
                  nudge_y = 0.05,
                  label.r = 0, #don't round corners of label boxes
                  min.segment.length = 0,
                  segment.size = 0.4,
                  segment.color = "dodgerblue")

insira a descrição da imagem aqui

invertdna
fonte
8
Caramba. Não, não apenas por princípio. Não sei como você está tramando, nem quão longe chegou, nem o que você mencionou funcionou no ggrepel com dados não geográficos. Você diz "isso funciona bem", mas não mostra o que é "isso", o que seria útil ver e desenvolver. Seria possível incluir um exemplo - sf e outros pacotes espaciais, como o spData, enviam dados de amostra ou você pode criar um pequeno objeto fictício de cadeia de linhas - mas, no momento, só podemos adivinhar quais deles ajudariam na sua situação, e isso é apenas não muito útil a longo prazo
camille
8
Se você não fornecer um exemplo reprodutível mínimo, estará basicamente pedindo aos outros que façam um para você. Caso contrário, eles geralmente não podem dar uma resposta muito boa. Nesse caso, isso significa que eles precisariam encontrar um arquivo de forma, descobrir como você está usando ggrepel, basicamente refazer o trabalho que você já fez. Isso torna muito menos provável que você tenha uma resposta útil.
Axeman
3
MWE agora incluído na pergunta. Desculpas pela reação; Eu não quero ser rude, e pensei muito sobre como não perder o tempo das pessoas antes de postar. Pareceu-me que eu estava pedindo uma resposta conceitual - ou seja, existe uma ferramenta desse tipo? - em vez de uma resposta específica para o meu projeto em particular.
invertdna 6/02
4
Legal, este é agora um bom exemplo e não o que eu teria inventado se você tivesse nos deixado adivinhar. Procurar algo conceitual, como se existe uma ferramenta, é considerado fora de tópico para SO; as perguntas são muito melhores quando estão vinculadas a um problema ou projeto específico. Para esclarecer, os rótulos estão inclinados ao longo da parte da cadeia de linhas da meta ou apenas para colocá-los perto dos recursos?
camille
8
@ Camille First: Peço desculpas pela minha primeira resposta. Hesitei em postar no SO porque é cheio de maldade e, ao me preparar para isso, me tornei a pessoa má. Sinto-me péssimo por isso e sinto muito. Quanto à questão em questão: os rótulos não precisam ser angulados; no contexto mais amplo (estradas e rios, principalmente), as cadeias de linhas são irregulares e, portanto, provavelmente o rótulo precisa estar em algum lugar ao longo da linha, mas (mais importante) paralelo à linha.
invertdna

Respostas:

8

Eu acho que tenho algo que pode funcionar para você. Tomei a liberdade de mudar seu exemplo para algo um pouco mais realista: alguns "rios" aleatórios feitos com passeios aleatórios suaves, cada um com 100 pontos:

library(tidyverse)
library(sf)
library(ggrepel)

BuncombeCounty <- st_read(system.file("shapes/", package = "maptools"), "sids") %>% 
                  filter(NAME == "Buncombe")
set.seed(120)

x1 <- seq(-82.795, -82.285, length.out = 100)
y1 <- cumsum(runif(100, -.01, .01))
y1 <- predict(loess(y1 ~ x1, span = 0.1)) + 35.6

x2 <- x1 + 0.02
y2 <- cumsum(runif(100, -.01, .01))
y2 <- predict(loess(y2 ~ x2, span = 0.1)) + 35.57

river_1 <- data.frame(x = x1, y = y1)     %>% 
           st_as_sf(coords = c("x", "y")) %>%
           st_coordinates()               %>%
           st_linestring()                %>%
           st_cast("LINESTRING") 

river_2 <- data.frame(x = x2, y = y2)     %>% 
           st_as_sf(coords = c("x", "y")) %>%
           st_coordinates()               %>%
           st_linestring()                %>%
           st_cast("LINESTRING") 

Podemos plotá-los conforme seu exemplo:

riverplot  <- ggplot() +
              geom_sf(data = BuncombeCounty) +
              geom_sf(data = river_1, colour = "blue", size = 2) +
              geom_sf(data = river_2, colour = "blue", size = 2)

riverplot

insira a descrição da imagem aqui

Minha solução é basicamente extrair pontos das cadeias de linhas e rotulá-los. Como na figura na parte superior da sua pergunta, você pode querer várias cópias de cada rótulo ao longo do comprimento da cadeia de linhas; portanto, se você quiser n rótulos, extraia n pontos igualmente espaçados.

É claro que você deseja rotular os dois rios de uma só vez sem que os rótulos colidam; portanto, você deve passar vários recursos geográficos como uma lista nomeada.

Aqui está uma função que faz tudo isso:

linestring_labels <- function(linestrings, n)
{
  do.call(rbind, mapply(function(linestring, label)
  {
  n_points <- length(linestring)/2
  distance <- round(n_points / (n + 1))
  data.frame(x = linestring[1:n * distance],
             y = linestring[1:n * distance + n_points],
             label = rep(label, n))
  }, linestrings, names(linestrings), SIMPLIFY = FALSE)) %>%
  st_as_sf(coords = c("x","y"))
}

Portanto, se colocarmos os objetos que queremos rotular em uma lista nomeada como esta:

river_list <- list("River 1" = river_1, "River 2" = river_2)

Então podemos fazer isso:

riverplot + 
   geom_label_repel(data = linestring_labels(river_list, 3),
                    stat = "sf_coordinates",
                    aes(geometry = geometry, label = label),
                    nudge_y = 0.05,
                    label.r = 0, #don't round corners of label boxes
                    min.segment.length = 0,
                    segment.size = 0.4,
                    segment.color = "dodgerblue")

insira a descrição da imagem aqui

Allan Cameron
fonte
2
sfheaders::sf_linestring(obj = data.frame(x = x1, y = y1))facilitará parte do sfcódigo de geração.
SymbolixAU