Placement automatique des étiquettes pour les cartes SIG dans R

9

Je crée des cartes SIG dans R en utilisant le sfpackage (et les packages associés) pour lire les fichiers de formes et ggplot2(et les amis) pour le traçage. Cela fonctionne bien, mais je ne trouve aucun moyen de créer (automatiquement / par programme) des emplacements d'étiquettes pour des entités telles que les rivières et les routes. Ces caractéristiques sont généralement des chaînes de lignes, avec des formes irrégulières. Voir l'image jointe par exemple de wikimedia.

entrez la description de l'image ici

Le ggrepelpackage fonctionne bien pour étiqueter les points de manière automatisée, mais cela n'a pas beaucoup de sens pour d'autres entités géographiques qui ne sont pas des points lat / long discrets.

Je pourrais imaginer faire cela en plaçant des étiquettes de texte individuelles sur chaque fonctionnalité individuellement, mais je cherche quelque chose de plus automatisé, si possible. Je me rends compte qu'une telle automatisation n'est pas un problème trivial, mais elle a été résolue auparavant (ArcGIS a apparemment un moyen de le faire avec une extension appelée maplex, mais je n'ai pas accès au logiciel, et j'aimerais rester dans R si possible).

Quelqu'un connaît-il une façon de procéder?

MWE ici:

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

entrez la description de l'image ici

invertdna
la source
8
Oui. Non, pas seulement par principe. Je ne sais pas comment vous complotez ou jusqu'où vous êtes allé, ou ce que vous mentionnez a fonctionné dans ggrepel avec des données non géographiques. Vous dites "ça marche bien" mais ne montrez pas ce que c'est, ce qui serait utile à voir et à développer. Il aurait été possible d'inclure un exemple — sf et d'autres packages spatiaux tels que les données d'échantillonnage du navire spData, ou vous pourriez créer un petit objet de chaîne de caractères factice — mais pour le moment, nous ne pouvons que deviner lequel de ces éléments aiderait votre situation, et c'est juste pas très utile à long terme
camille
8
Si vous ne fournissez pas d'exemple reproductible minimal, vous demandez essentiellement aux autres d'en créer un pour vous. Sinon, ils ne peuvent généralement pas donner une très bonne réponse. Dans ce cas, cela signifie qu'ils auraient besoin de trouver un fichier de formes, de comprendre comment vous utilisez ggrepel, essentiellement de refaire le travail que vous avez déjà fait. Cela rend beaucoup moins probable que vous obtiendrez une réponse utile.
Axeman
3
MWE maintenant inclus dans la question. Toutes mes excuses pour la réaction; Je ne veux pas être impoli, et j'ai réfléchi à la façon de ne pas perdre le temps des gens avant de poster. Il me semblait que je demandais une réponse conceptuelle - c'est-à-dire, un tel outil existe-t-il? - plutôt qu'une réponse spécifique à mon projet particulier.
invertdna
4
Cool, c'est maintenant un bon exemple et non celui que j'aurais trouvé si vous nous aviez laissé deviner. La recherche de quelque chose de conceptuel comme l'existence d'un outil est considérée comme hors sujet pour SO; les questions sont bien meilleures lorsqu'elles sont liées à un problème ou un projet spécifique. Pour clarifier, est-ce que les étiquettes sont inclinées le long de la partie de chaîne de l'objectif ou simplement pour les placer près des entités?
camille
8
@camille First: Je m'excuse vraiment pour ma première réponse. J'ai hésité à poster sur SO parce que c'est plein de méchanceté, et en me préparant pour cela, je suis devenu le méchant moi-même. Je me sens mal à ce sujet et je suis vraiment désolé. Quant à la question qui se pose: les étiquettes n'ont pas besoin d'être inclinées; dans le contexte plus large (routes et rivières, principalement), les lignes linéaires sont irrégulières, et donc probablement l'étiquette doit juste être quelque part le long de la ligne, mais (surtout) parallèle à la ligne.
invertdna

Réponses:

8

Je pense que j'ai quelque chose qui pourrait fonctionner pour vous. J'ai pris la liberté de changer votre exemple pour quelque chose d'un peu plus réaliste: quelques "rivières" aléatoires faites avec des marches aléatoires lissées, chacune de 100 points de long:

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

Nous pouvons les tracer selon votre exemple:

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

entrez la description de l'image ici

Ma solution consiste essentiellement à extraire des points des chaînes de lignes et à les étiqueter. Comme l'image en haut de votre question, vous voudrez peut-être plusieurs copies de chaque étiquette sur la longueur de la chaîne de lignes, donc si vous voulez n étiquettes, vous extrayez juste n points également espacés.

Bien sûr, vous voulez pouvoir étiqueter les deux rivières à la fois sans que les étiquettes ne se heurtent, vous devrez donc être en mesure de passer plusieurs caractéristiques géographiques sous forme de liste nommée.

Voici une fonction qui fait tout cela:

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

Donc, si nous mettons les objets que nous voulons étiqueter dans une liste nommée comme ceci:

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

Ensuite, nous pouvons le faire:

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

entrez la description de l'image ici

Allan Cameron
la source
2
sfheaders::sf_linestring(obj = data.frame(x = x1, y = y1)) facilitera certains des sf code de génération.
SymbolixAU