Automatyczne umieszczanie etykiet dla map GIS w R.

9

Tworzę mapy GIS w R, używając sfpakietu (i powiązanych pakietów) do czytania w plikach kształtów oraz ggplot2(i znajomych) do kreślenia. Działa to dobrze, ale nie mogę znaleźć (automatycznie / programowo) sposobu umieszczania etykiet dla obiektów takich jak rzeki i drogi. Te cechy są zwykle liniami o nieregularnych kształtach. Zobacz obraz załączony na przykład z wikimedia.

wprowadź opis zdjęcia tutaj

ggrepelPakiet działa dobrze do znakowania punktów w sposób zautomatyzowany, ale to nie ma większego sensu dla innych obiektów geograficznych, które nie są dyskretne punkty Lat / Long.

Mógłbym to sobie wyobrazić, umieszczając indywidualne etykiety tekstowe dla każdej funkcji osobno, ale szukam czegoś bardziej zautomatyzowanego, jeśli to możliwe. Zdaję sobie sprawę, że taka automatyzacja nie jest trywialnym problemem, ale została już wcześniej rozwiązana (ArcGIS najwyraźniej ma taką możliwość z rozszerzeniem o nazwie maplex, ale nie mam dostępu do oprogramowania i chciałbym zostać w nim R, jeśli to możliwe).

Czy ktoś wie, jak to zrobić?

MWE tutaj:

#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")

wprowadź opis zdjęcia tutaj

invertdna
źródło
8
Yikes. Nie, nie tylko z zasady. Nie wiem, jak knujesz, jak daleko zaszedłeś, ani co wspominałeś, działając w ggrepel z danymi niegeograficznymi. Mówisz „to działa dobrze”, ale nie pokazuj, co to „to”, co byłoby pomocne, aby je zobaczyć i zbudować. Można by podać przykład - sf i inne pakiety przestrzenne, takie jak przykładowe dane statku spData, lub można zrobić mały obojętny obiekt liniowy - ale w tej chwili możemy tylko zgadywać, który z nich pomógłby w twojej sytuacji, a to po prostu niezbyt przydatne długoterminowe
Camille
8
Jeśli nie podasz minimalnego odtwarzalnego przykładu, w zasadzie prosisz innych o zrobienie go dla ciebie. W przeciwnym razie zazwyczaj nie mogą dać bardzo dobrej odpowiedzi. W tym przypadku oznacza to, że będą musieli znaleźć plik kształtu, dowiedzieć się, jak używasz ggrepel, w zasadzie ponownie wykonać pracę, którą już wykonałeś. To sprawia, że ​​znacznie mniej prawdopodobne jest, że otrzymasz użyteczną odpowiedź.
Axeman
3
MWE jest teraz uwzględnione w pytaniu. Przepraszamy za reakcję; Nie chcę być niegrzeczny i zastanawiałem się, jak nie marnować czasu przed publikowaniem postów. Wydawało mi się, że pytam o konceptualną odpowiedź - tj. Czy takie narzędzie istnieje? - zamiast odpowiedzi specyficznej dla mojego konkretnego projektu.
invertdna
4
Fajnie, to jest teraz dobry przykład, a nie ten, który wymyśliłbym, gdybyśmy zgadli. Szukanie czegoś konceptualnego, takiego jak to, czy narzędzie istnieje, jest uważane za nie na temat dla SO; pytania są znacznie lepsze, gdy są powiązane z konkretnym problemem lub projektem. Aby to wyjaśnić, czy etykiety są ustawione pod kątem wzdłuż części linii bramkowej celu, czy po prostu umieszczają je w pobliżu obiektów?
camille
8
@camille First: Naprawdę przepraszam za pierwszą odpowiedź. Zawahałem się, aby opublikować post na SO, ponieważ jest on pełen podłości i przygotowując się do tego, sam stałem się podły. Czuję się z tego powodu okropnie i bardzo mi przykro. Co do pytania: etykiety nie muszą być ustawione pod kątem; w szerszym kontekście (głównie drogi i rzeki) pasy są nieregularne, więc prawdopodobnie etykieta musi znajdować się gdzieś wzdłuż linii, ale (co ważne) równolegle do linii.
invertdna

Odpowiedzi:

8

Myślę, że mam coś, co może ci pomóc. Pozwoliłem sobie na zmianę twojego przykładu na coś bardziej realistycznego: kilka losowych „rzek” wykonanych za pomocą wygładzonych losowych spacerów, każde o długości 100 punktów:

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") 

Możemy wykreślić je według twojego przykładu:

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

wprowadź opis zdjęcia tutaj

Moje rozwiązanie polega w zasadzie na wydobywaniu punktów z linii i oznaczaniu ich. Podobnie jak obrazek na górze pytania, możesz potrzebować wielu kopii każdej etykiety wzdłuż długości linii, więc jeśli chcesz n etykiet, po prostu wyodrębnij n równo rozmieszczonych punktów.

Oczywiście chcesz mieć możliwość oznakowania obu rzek jednocześnie, bez kolizji etykiet, więc musisz przekazać wiele obiektów geograficznych jako listę nazwaną.

Oto funkcja, która robi to wszystko:

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"))
}

Więc jeśli umieścimy obiekty, które chcemy oznaczyć na nazwanej liście, takiej jak ta:

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

Następnie możemy to zrobić:

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")

wprowadź opis zdjęcia tutaj

Allan Cameron
źródło
2
sfheaders::sf_linestring(obj = data.frame(x = x1, y = y1))ułatwi część sfgenerowania kodu.
SymbolixAU