2  Première manipulation du RPG

Mots clés

RPG, PostgreSQL, PostGIS, manipulation

Dans cette application, nous souhaitons effectuer des premières requêtes géographiques sur une base PostgreSQL contenant les données du RPG. L’objectif est d’examiner les cultures à proximité d’un point géographique donné, puis de comparer la composition observée avec les compositions départementale et nationale. Dans un second temps, on verra comment développer un tableau de bord pour obtenir une visualisation interactive des résultats de ces requêtes.

Comme expliqué dans l’application précédente, la base PostgreSQL est accessible depuis tous les services interactifs du cluster. Des indications sur la manière de s’y connecter figurent sur la page. Pour rappel, le schéma public est ouvert à tous en écriture pour créer des tables temporaires. Pour ces tables, essayez d’utiliser des noms uniques pour éviter d’entrer en conflit avec d’autres équipes.

2.1 Récupération des coordonnées d’un point

Commençons par récupérer les coordonnées d’un point sur Google Maps et par les stocker dans un objet spatial grâce au package sf. Choisissez un point sur la carte et copiez ses coordonnées dans le presse-papier grâce à un clic droit. Créez ensuite deux variables lat et lon contenant la latitude et la longitude du point, ainsi qu’une variable rayon contenant le rayon souhaité pour la suite de l’analyse (en mètres).

Créez ensuite un objet point contenant les informations spatiales du point et le rayon choisi, à partir d’un data.frame et en utilisant la fonction st_as_sf. On projettera les coordonnées dans le système Lambert 93 (EPSG:2154) grâce à la fonction st_transform. Notez que les coordonnées récupérées sur Google Maps sont des coordonnées GPS, un système de projection différent.

Solution
voir le code
library(tidyverse) 
library(aws.s3)
library(sf)
library(RPostgres)
library(janitor)
library(kableExtra)
library(leaflet)
library(htmlwidgets)
library(RColorBrewer)

coord_gmaps <- "43.447894436406216, 1.2886163291688764"
lat <- as.numeric(str_split(coord_gmaps, fixed(","), simplify = TRUE)[,1])
lon <- as.numeric(str_split(coord_gmaps, fixed(","), simplify = TRUE)[,2])
rayon <- 10000

# Création d'une table sf «point» avec les coordonnées saisies
# Transformation des coordonnées en système de proj 2154 (Lambert II) 
point <- data.frame(lon, lat, rayon) %>% 
  st_as_sf(coords = c("lon","lat"), crs = "EPSG:4326") %>%
  mutate(coord_pt_gps = st_as_text(geometry)) %>% 
  st_transform("EPSG:2154") %>% 
  st_sf() %>%
  clean_names() %>% 
  rename(geom = geometry)

2.1.1 Connexion au serveur PostgreSQL

La table des parcelles agricoles 2021 se trouve sur un serveur PostgreSQL muni de l’extension PostGIS. Connectez vous à ce serveur PostgreSQL en utilisant des variables d’environnement USER_POSTGRESQL, PASS_POSTGRESQL et HOST_POSTGRESQL bien configurées.

Solution
voir le code
# Connexion à PostgreSQL
cnx <- dbConnect(Postgres(),
                 user = Sys.getenv("USER_POSTGRESQL"),
                 password = Sys.getenv("PASS_POSTGRESQL"),
                 host = Sys.getenv("HOST_POSTGRESQL"),
                 dbname = "defaultdb",
                 port = 5432,
                 check_interrupts = TRUE)

2.1.2 Sélection des parcelles situées autour d’un point

On souhaite maintenant requêter la base de données PostgreSQL pour récupérer les parcelles se situant dans un cercle du rayon choisi autour du point choisi. Pour cela, une possibilité est de passer par la création d’une table intermédiaire à partir de l’objet point défini précédemment (la fonction write_sf permet d’écrire des données spatiales dans une table PostGIS), puis d’utiliser la fonction ST_DWithin dans une requête. Stockez les résultats de la requête dans une variable parc_prox grâce à la fonction st_read.

Solution
voir le code
# Optionnel, suppression de la table `point` si elle existe
res <- dbSendQuery(cnx, "DROP TABLE IF EXISTS public.point CASCADE;")

# Ecriture de la table point dans une table PostGIS
write_sf(point, cnx, Id(schema = "public", table = "point"))

# Envoi de la requête de découpage du RPG autour du point sur PostGIS
query <- "SELECT row_number() OVER () AS row_id, p.coord_pt_gps, p.rayon, r.*  
    FROM public.point p, rpg.parcelles r 
    WHERE ST_DWithin(p.geom, r.geom, p.rayon);"

parc_prox <- st_read(cnx, query = query)

2.1.3 Visualisation avec une carte interactive leaflet

On souhaite maintenant utiliser la librairie leaflet pour créer une visualisation interactive des données. On va vouloir afficher sur la carte interactive les libellés des cultures. Pour ceci, on récupère les groupes de cultures agrégés sur l’espace de stockage du SSP Cloud, avec un léger prétraitement, comme indiqué ici :

# Récupération des libellés des différentes cultures
lib_cult <- s3read_using(FUN = read_csv2,
                         object = "2023/sujet2/diffusion/ign/rpg/REF_CULTURES_GROUPES_CULTURES_2020.csv",
                         col_types = cols(.default = col_character()),
                         bucket = "projet-funathon",
                         opts = list("region" = "")) %>% clean_names()


lib_group_cult <- lib_cult %>% 
  select(code_groupe_culture, libelle_groupe_culture) %>% 
  distinct(code_groupe_culture, .keep_all=T)

lib_group_cult %>% kable()
code_groupe_culture libelle_groupe_culture
1 Blé tendre
2 Maïs grain et ensilage
3 Orge
4 Autres céréales
5 Colza
6 Tournesol
7 Autres oléagineux
8 Protéagineux
9 Plantes à fibres
11 Gel (surfaces gelées sans production)
14 Riz
15 Légumineuses à grains
16 Fourrage
17 Estives et landes
18 Prairies permanentes
19 Prairies temporaires
20 Vergers
21 Vignes
22 Fruits à coque
23 Oliviers
24 Autres cultures industrielles
25 Légumes ou fleurs
26 Canne à sucre
28 Divers

On va créer le widget grâce à la fonction leaflet, qui prend en argument une table sf. On peut ensuitea utiliser les fonctions addTiles (ajout d’un fond de carte) et addPolygons qui permet de personnaliser l’affichage des parcelles, notamment grâce à l’argument fillColor. Nous proposons une solution juste en dessous, mais n’hésitez pas à expérimenter !

Note

Pour utiliser leaflet, il faut que les données spatiales soient en coordonnées GPS.

Solution
voir le code
# Création d'une palette de couleurs associée au groupe de culture
factpal <- colorFactor("Paired", parc_prox$code_group)

# Transformation de la projection car leaflet ne connait que le WGS 84
parc_prox <- parc_prox %>% st_transform(4326)

# Pour ajouter un marqueur du point
pt_mark <- point %>% st_transform(4326)

# Ajout du libellé des cultures
parc_prox_lib <- parc_prox %>% 
  left_join(lib_cult %>% select(-code_groupe_culture), by = c("code_cultu" = "code_culture")) 

# Création d'un label ad hoc à afficher en surbrillance au passage de la souris sur la carte
labels <- sprintf("<strong>id_parcel : </strong>%s<br/>
                  <strong>Groupe culture : </strong>%s<br/>
                  <strong>Culture : </strong>%s<br/>
                  <strong>Surface (ha) : </strong>%s<br/>
                  <strong>Département : </strong>%s<br/>
                  <strong>Commune : </strong>%s<br/>",
                  parc_prox$id_parcel,
                  parc_prox_lib$libelle_groupe_culture,
                  parc_prox_lib$libelle_culture,
                  parc_prox$surf_parc,
                  parc_prox$insee_dep,
                  parc_prox$nom_com) %>% 
  lapply(htmltools::HTML)

# Création de la carte
carte_parc_prox_html <- leaflet(parc_prox_lib) %>% 
  addTiles("http://wxs.ign.fr/essentiels/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE=normal&TILEMATRIXSET=PM&FORMAT=image/jpeg&LAYER=ORTHOIMAGERY.ORTHOPHOTOS&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}") %>%
  addPolygons(fillColor = ~factpal(code_group),
              weight = 2,
              opacity = 1,
              color = "#ffd966",
              dashArray = "3",
              fillOpacity = 0.5,
              highlight = highlightOptions(
                weight = 5,
                color = "#A40000",
                dashArray = "",
                fillOpacity = 0.0,
                bringToFront = TRUE),
              label = labels,
              labelOptions = labelOptions(
                style = list("font-weight" = "normal", padding = "3px 8px"),
                textsize = "15px",
                direction = "auto",
                encoding="UTF-8")) %>% 
  addMarkers(data = pt_mark, ~lon, ~lat, popup = ~coord_pt_gps, label = ~coord_pt_gps)

# Pour sauvegarder la carte
# saveWidget(widget = carte_parc_prox_html, file = "carte_parc_prox.html")

carte_parc_prox_html

2.1.4 Composition des parcelles agricoles récupérées

On souhaite calculer des statistiques sur les parcelles récupérées. Dans une table t1, inclure le nombre de parcelles par groupe de culture, ainsi que le nombre total de parcelles parmi l’échantillon issu de la requête. Faites la même chose sur la surface des parcelles, puis calculez la surface moyenne par parcelle et groupe de culture.

Solution
voir le code
t1 <- parc_prox %>%
  st_drop_geometry() %>%
  count(code_group) %>% 
  add_tally(n) %>% 
  mutate(n_pct = round(100 * n / nn, 1)) %>% 
  select(-nn) %>%
  rename(n_parcelles = n) %>%
  cbind(
    # Surfaces
    parc_prox %>%
      st_drop_geometry() %>%
      count(code_group, wt = surf_parc) %>% 
      add_tally(n) %>% 
      mutate(surf_pct = round(100 * n / nn, 1)) %>%
      select(-nn) %>%  
      rename(surf_parc_ha = n) %>%
      select(surf_parc_ha, surf_pct)
  ) %>%
  left_join(lib_group_cult, by = c("code_group" = "code_groupe_culture")) %>% 
  select(code_group, libelle_groupe_culture, everything()) %>% 
  arrange(desc(surf_parc_ha)) %>% 
  adorn_totals() %>% 
  mutate(taille_moy_parc = round(surf_parc_ha / n_parcelles, 1))

t1  %>% 
  setNames(c("Code", "Groupe de cultures", "Nombre de parcelles", "(%)", "Surface (ha)", "Surface (%)", "Taille moyenne (ha)")) %>% 
  kable(
    format="html",
    caption="<span style='font-size:medium'>Groupes de cultures <strong>locales</strong> par surfaces décroissantes</span>",
    format.args = list(decimal.mark = ",", big.mark = " "),
    booktabs = TRUE) %>%
  kable_styling(font_size = 15) %>% 
  gsub("font-size: initial !important;",
       "font-size: 20pt !important;",.)%>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>% 
  row_spec(nrow(t1), bold = T, color = "white", background = "grey")
Structure des cultures au niveau local
Code Groupe de cultures Nombre de parcelles (%) Surface (ha) Surface (%) Taille moyenne (ha)
1 Blé tendre 631 10,9 3 044,52 17,6 4,8
2 Maïs grain et ensilage 368 6,4 2 922,39 16,9 7,9
4 Autres céréales 404 7,0 1 826,24 10,6 4,5
11 Gel (surfaces gelées sans production) 1 437 24,9 1 494,91 8,7 1,0
6 Tournesol 308 5,3 1 430,40 8,3 4,6
18 Prairies permanentes 573 9,9 1 260,24 7,3 2,2
7 Autres oléagineux 184 3,2 1 237,37 7,2 6,7
3 Orge 214 3,7 947,61 5,5 4,4
16 Fourrage 234 4,1 804,31 4,7 3,4
19 Prairies temporaires 302 5,2 802,74 4,6 2,7
8 Protéagineux 116 2,0 538,13 3,1 4,6
5 Colza 106 1,8 468,59 2,7 4,4
28 Divers 754 13,1 184,22 1,1 0,2
15 Légumineuses à grains 34 0,6 147,28 0,9 4,3
25 Légumes ou fleurs 38 0,7 46,95 0,3 1,2
21 Vignes 21 0,4 37,31 0,2 1,8
17 Estives et landes 16 0,3 33,19 0,2 2,1
24 Autres cultures industrielles 23 0,4 32,70 0,2 1,4
20 Vergers 7 0,1 4,76 0,0 0,7
Total - 5 770 100,0 17 263,86 100,1 3,0

2.1.5 Comparaison avec la répartition des cultures au niveau départemental et national

On va vouloir comparer la composition des parcelles à proximité du point choisi avec la composition au niveaux départemental et national. Pour ce faire, commencer par faire une jointure spatiale sur PostGIS pour récupérer le département du point. Les géométries des départements se récupèrent avec la commande suivante :

# Couche département pour récupérer le département du point
dep <- s3read_using(
    FUN = sf::read_sf,
    layer = "departement",
    object = "2023/sujet2/diffusion/ign/adminexpress_cog_simpl_000_2023.gpkg",
    bucket = "projet-funathon",
    opts = list("region" = "")) %>% 
  st_transform(2154)

Pour faire la jointure spatiale, on pourra utiliser la fonction st_join.

Solution
voir le code
# Jointure spatiale
df <- point %>% st_join(dep) %>% st_drop_geometry() %>% select(insee_dep)
dep_pt <- df[1,1]

Pour ne pas avoir à refaire de gros calculs sur la table RPG, les statistiques départementales et nationales sont disponibles directement grâce aux commandes suivantes :

# Récupération des statistiques départementales
stat_dep_pt <- s3read_using(
  FUN = readr::read_rds,
  object = "2023/sujet2/diffusion/resultats/stat_group_cult_by_dep.rds",
  bucket = "projet-funathon",
  opts = list("region" = ""))

# Récupération des statistiques nationales
stat_fm <- s3read_using(
  FUN = readr::read_csv,
  object = "2023/sujet2/diffusion/resultats/stat_group_cult_fm.csv",
  col_types = cols(code_group = col_character()),
  bucket = "projet-funathon",
  opts = list("region" = "")) %>% 
  select(code_group, libelle_groupe_culture, pct_surf) %>% 
  rename(pct_surf_fm = pct_surf)

Sélectionnez les statistiques du département concerné et appariez statistiques de surfaces locales, départementales et nationales par groupe de culture dans un même data.frame à afficher, comme ci-dessous.

Solution
voir le code
# Calcul des % surfaces autour du point
stat_pt <- parc_prox %>%
  st_drop_geometry() %>% 
  count(code_group, wt = surf_parc) %>%
  add_tally(n) %>% 
  mutate(pct_surf_local = round(100 * n / nn, 1)) %>%
  select(code_group, pct_surf_local) 

# Récupération des statistiques du département concerné
stat_dep_pt <- stat_dep_pt %>% 
  filter(insee_dep %in% dep_pt) %>% 
  select(insee_dep, code_group, libelle_groupe_culture, pct_surf) %>% 
  rename(pct_surf_dep = pct_surf)

# Appariement des statistiques locale, départementale et nationale
stat_compar <- stat_fm %>% 
  left_join(stat_dep_pt %>% select(code_group, pct_surf_dep), by = "code_group") %>% 
  left_join(stat_pt , by = "code_group") %>% 
  select(libelle_groupe_culture, pct_surf_local, pct_surf_dep, pct_surf_fm) %>% 
  arrange(desc(pct_surf_local)) %>%
  adorn_totals() 

stat_compar %>% 
  setNames(c("Groupe de cultures","Surf. locales (%)", "Surf. départ. (%)","Surf. France m. (%)")) %>%
  kable(
    format="html",
    caption="<span style='font-size:medium'>Comparaison des surfaces locales, départementales et nationales</span>",
    format.args = list(decimal.mark = ",", big.mark = " "),
    booktabs = TRUE) %>%
  kable_styling(font_size = 15) %>% 
  gsub("font-size: initial !important;",
       "font-size: 20pt !important;",.)%>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>% 
  row_spec(nrow(stat_compar), bold = T, color = "white", background = "grey")
Structure des cultures
Groupe de cultures Surf. locales (%) Surf. départ. (%) Surf. France m. (%)
Blé tendre 17,6 14,3 17,7
Maïs grain et ensilage 16,9 8,6 10,0
Autres céréales 10,6 12,4 3,9
Gel (surfaces gelées sans production) 8,7 4,4 1,6
Tournesol 8,3 12,6 2,5
Prairies permanentes 7,3 17,9 27,7
Autres oléagineux 7,2 3,1 0,7
Orge 5,5 3,5 6,2
Fourrage 4,7 5,9 3,7
Prairies temporaires 4,6 4,2 5,3
Protéagineux 3,1 1,8 1,2
Colza 2,7 1,8 3,5
Divers 1,1 1,3 1,3
Légumineuses à grains 0,9 0,3 0,2
Légumes ou fleurs 0,3 0,3 1,5
Estives et landes 0,2 6,9 8,1
Vignes 0,2 0,4 2,2
Autres cultures industrielles 0,2 0,1 1,8
Vergers 0,0 0,1 0,4
Plantes à fibres NA 0,0 0,4
Riz NA NA 0,0
Fruits à coque NA 0,0 0,2
Oliviers NA 0,0 0,0
Total 100,1 99,9 100,1

2.1.6 Graphique de comparaison des cultures au niveau local, départemental et national

On souhaite faire un graphique avec ggplot2 pour visualiser la comparaison établie ci-dessus. Utilisez la fonction geom_col pour obtenir le graphique affiché ci-dessous.

Solution
voir le code
# Sélection des 10 groupes de cultures les plus répandus au niveau local 
tab <- stat_compar %>%
  filter(libelle_groupe_culture != "Total") %>%
  slice_head(n=10) %>% 
  rename(local = pct_surf_local, departement = pct_surf_dep, france = pct_surf_fm)

# Transposition de la table pour rassembler toutes les valeurs dans une seule variable value
tab_piv <- tab %>% pivot_longer(!libelle_groupe_culture) %>% rename(secteur = name) 

# Valeurs manquantes à
tab_piv[is.na(tab_piv)] <- 0

# On réordonne les secteurs dans le "bon" ordre, avec factor
tab_piv$secteur <- factor(
  tab_piv$secteur,
  levels = c("france", "departement", "local"))
tab_piv <- tab_piv %>% arrange(desc(secteur), desc(value))

# On réordonne les cultures par surface décroissante au niveau local, avec factor
x <- tab_piv %>% filter(secteur == "local") %>% arrange(value) %>% select(libelle_groupe_culture)
y <- pull(x, libelle_groupe_culture)

tab_piv$libelle_groupe_culture <- factor(tab_piv$libelle_groupe_culture, levels = y)

# Visualisation avec `geom_col`
p <- ggplot(tab_piv, aes(x = libelle_groupe_culture,
                         y = value, 
                         fill = factor(
                           secteur,
                           levels = c("france", "departement", "local")))) + 
  geom_col(position = "dodge") +
  labs(title = "Surfaces comparées des 10\nprincipales cultures locales, en %", x="Culture", y = "%", fill = "Secteur") +
  theme_classic()

# Flip du graphique pour avoir des barres horizontales  
p + coord_flip()

2.1.7 Graphique par secteur

La fonction facet_wrap permet d’afficher plusieurs graphiques côte-à-côte. En utilisant cette fonction, réalisez le graphique affiché ci-dessous.

Solution
voir le code
# Visualisation avec `geom_col` et `facet_wrap`   
ggplot(tab_piv, 
       aes(x = libelle_groupe_culture,
           y = value)) + 
  geom_col(fill = "lightblue", colour = "black", position = "dodge") +
  labs(title = "Surface par culture", x= "Culture", y = "%", fill = "Secteur") +
  geom_text(aes(label = value), hjust = -0.3, size = 8/.pt, colour = "black") +
  theme_classic() + coord_flip() + 
  facet_wrap(~secteur, nrow=3, scales='free')

2.2 Création d’un dashboard de visualisation

On souhaiterait intégrer les analyses faites ci-dessus à un tableau de bord qui offrirait des visualisations interactives. Une solution est d’utiliser shiny, une librairie qui permet la création de telles applications avec R notamment.

Une application shiny simple a deux composants principaux :

  • Un objet d’interface utilisateur (UI) qui contrôle la disposition et l’apparence du tableau de bord ;
  • Une fonction serveur qui contient les instructions nécessaires au fonctionnement de l’application.

Lorsque ces deux composants sont définis, l’application est lancée à l’aide d’une simple instruction runApp. Ici, on aimerait bien aller vers une première version de tableau de bord, qui afficherait au départ une carte interactive et laisserait aussi à l’utilisateur de choisir un rayon. Une fois ce rayon choisi et un point sélectionné grâce à un clic sur la carte, on souhaiterait (comme affiché ci-dessous) :

  • Afficher sur la carte les parcelles se situant à une distance inférieure au rayon choisi du point spécifié ;
  • Afficher des statistiques sur les parcelles en questions.

Shiny screenshot Ainsi notre UI sera un objet fluidPage composé de trois éléments, et dans la fonction de serveur il y aura deux évènements distincts à observer. Pour développer l’application, créez un répertoire my_app et à l’intérieur 3 fichiers, un fichier ui.R, un fichier server.R et un fichier utils.R (dans lequel se trouveront les fonctions utilitaires).

2.3 UI

Le fichier UI aura la forme suivante :

library(shiny)

# Define UI
ui <- fluidPage(
  ...
)

A l’intérieur de la fonction fluidPage et grâce à la documentation de Shiny, ajoutez les 3 éléments qui constitueront le tableau de bord : un output de type “carte leaflet”, un champ d’input numérique, et un output de type “table”.

Solution
voir le code
library(shiny)

# Define UI
ui <- fluidPage(
  leafletOutput("map", height = 800),
  numericInput("buffer_radius", "Rayon (en km) :", value = 5),
  tableOutput("table")
)

2.4 Serveur

Le code de la fonction serveur figure ci-dessous dans son intégralité, et l’objectif de cette partie va être d’implémenter dans utils.R les différentes fonctions appelées dans le code :

  • connect_to_db : une fonction qui renvoie une connexion à la base de données ;
  • query_db : une fonction qui interroge la base de données pour récupérer les parcelles se situant à l’intérieur d’un cercle de rayon radius d’un point défini par une latitude et une longitude ;
  • plot_surroundings : une fonction qui prend en entrée la sortie de la fonction query_db et une carte leaflet et qui ajoute une couche comportant les polygones des parcelles concernées ;
  • compute_stats : une fonction qui prend en entrée la sortie de la fonction query_db et renvoie des statistiques sur les parcelles concernées.
library(shiny)
library(leaflet)


# Définition du serveur
server <- function(input, output) {
  
  # Rendre la carte
  output$map <- renderLeaflet({
    leaflet() %>%
      addTiles("http://wxs.ign.fr/essentiels/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&STYLE=normal&TILEMATRIXSET=PM&FORMAT=image/jpeg&LAYER=ORTHOIMAGERY.ORTHOPHOTOS&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}") %>%
      setView(lng = -1.4932577154775046, lat = 46.46946179131805, zoom = 12)
  })
  
  # Connexion à la base de données
  cnx <- connect_to_db()
  
  # Initialisation d'une "reactive value" pour le point sélectionné
  selectedPoint <- reactiveValues(lat = NULL, lng = NULL)
  
  # Gestion de l'évènement "clic"
  observeEvent(input$map_click, {
    
    clickData <- input$map_click
    if (!is.null(clickData)) {
      # Stockage des coordonnées du point
      selectedPoint$lat <- clickData$lat
      selectedPoint$lng <- clickData$lng
      
      buffer_radius <- input$buffer_radius
      sf <- query_db(cnx, selectedPoint$lat, selectedPoint$lng, buffer_radius)

      # Mise à jour de la carte
      leafletProxy("map") %>%
        clearMarkers() %>%
        clearShapes() %>%
        addMarkers(lng = selectedPoint$lng, lat = selectedPoint$lat) %>%
        plot_surroundings(sf)
      
      # Calculs sur les données de parcelles
      df <- compute_stats(sf)
      
      # Update de la table affichée
      output$table <- renderTable({
        df
      })
    }
  })
  
  observeEvent(input$buffer_radius, {
    # Vérification qu'un point a été sélectionné
    if (!is.null(selectedPoint$lat) && !is.null(selectedPoint$lng)) {
      # Requête avec le nouveau rayon
      buffer_radius <- input$buffer_radius
      sf <- query_db(cnx, selectedPoint$lat, selectedPoint$lng, buffer_radius)
      
      # Mise à jour de la carte
      leafletProxy("map") %>%
        clearShapes() %>%
        plot_surroundings(sf)
      
      # Calculs sur les données de parcelles
      df <- compute_stats(sf)
      
      # Update de la table affichée
      output$table <- renderTable({
        df
      })
    }
  })
}

Proposer une implémentation des fonctions connect_to_db, query_db et compute_stats décrites ci-dessus et utilisées dans la fonction “serveur”.

2.4.1 connect_to_db

La fonction connect_to_db renvoie une connexion à la base de données PostgreSQL, comme vu précédemment. Les identifiants (user et password sont respectivement stockés dans les variables d’environnement USER_POSTGRESQL et PASS_POSTGRESQL.)

Solution
voir le code
#' Connection au serveur PostgreSQL. Le mot de passe doit être stocké dans la
#' variable d'environnement PASS_POSTGRESQL.
#' 
#' @returns Connexion au serveur.
connect_to_db <- function() {
  # Connection à PostgreSQL
  cnx <- dbConnect(Postgres(),
                   user = Sys.getenv("USER_POSTGRESQL"),
                   password = Sys.getenv("PASS_POSTGRESQL"),
                   host = "postgresql-100400.projet-funathon",
                   dbname = "defaultdb",
                   port = 5432,
                   check_interrupts = TRUE)
  
  return(cnx)
}

2.4.2 query_db

On souhaite implémenter la fonction suivante :

#' Requête la table `parcelles` pour récupérer les parcelles qui se situent
#' dans un certain rayon autour d'un point repéré par une latitude et 
#' une longitude.
#' 
#' @param cnx Connexion à PostgreSQL.
#' @param lat Latitude.
#' @param lng Longitude.
#' @param radius Rayon.
#' @returns Objet `sf` avec les données des parcelles concernées.
query_db <- function(cnx, lat, lng, radius) {
  ...
}

On pourra notamment utiliser plusieurs fonctions PostGIS :

  • ST_MakePoint qui permet de créer une géométrie POINT;
  • ST_SetSRID qui permet de définir le système de coordonnées pour une géométrie;
  • ST_Buffer qui calcule un POLYGON ou un MULTIPOLYGON qui représente tous les points dont la distance par rapport à une géométrie/géographie est inférieure ou égale à une distance donnée;
  • ST_Intersects qui compare deux géométries et renvoie true si elles ont une intersection non-nulle;

ainsi que des fonction de la librairie sf.

Solution
voir le code
#' Requête la table `parcelles` pour récupérer les parcelles qui se situent
#' dans un certain rayon autour d'un point repéré par une latitude et 
#' une longitude.
#' 
#' @param cnx Connexion à PostgreSQL.
#' @param lat Latitude.
#' @param lng Longitude.
#' @param radius Rayon.
#' @returns Objet `sf` avec les données des parcelles concernées.
query_db <- function(cnx, lat, lng, radius) {
  # Les données spatiales sur PostgreSQL sont stockées en Lambert 93.
  # Pour faire le join on veut donc projeter les coordonnées `lat`` et `lng`
  postgis_crs <- "EPSG:2154"
  coordinates <- data.frame(lng = c(lng), lat = c(lat)) %>%
    st_as_sf(coords = c("lng", "lat"), remove = TRUE) %>%
    st_set_crs("EPSG:4326") %>%
    st_transform(postgis_crs)
  
  # Requête PostgreSQL
  query <- sprintf(
    "SELECT * FROM rpg.parcelles WHERE ST_Intersects(geom, ST_Buffer(ST_SetSRID(ST_MakePoint(%f, %f), 2154), %.0f));",
    st_coordinates(coordinates)[1],
    st_coordinates(coordinates)[2],
    radius*1000)
  
  # Récupération des résultats
  sf <- st_read(
    cnx,
    query = query
  )
  
  return(sf)
}

2.4.3 compute_stats

On souhaite implémenter la fonction suivante :

#' Crée la table à afficher sur l'application grâce à des calculs sur les
#' données requêtées depuis PostgreSQL.
#' 
#' @param sf Données spatiales.
#' @returns data.frame à afficher.
compute_stats <- function(sf) {
  ...
}

qui calcule sur une table spatiale renvoyée par la fonction query_db (ensemble des parcelles se situant autour d’un point donné) les statistiques suivantes par groupe de culture agregé :

  • Nombre de parcelles;
  • Pourcentage des parcelles parmi toutes les parcelles;
  • Surface des parcelles;
  • Surface des parcelles rapportée à la surface de toutes les parcelles;
  • Surface moyenne d’une parcelle.

On récupère les groupes de cultures agrégés sur l’espace de stockage du SSP Cloud et avec un léger prétraitement, comme précédemment :

# Récupération des libellés des différentes cultures
lib_cult <- s3read_using(FUN = read_csv2,
                         object = "2023/sujet2/diffusion/ign/rpg/REF_CULTURES_GROUPES_CULTURES_2020.csv",
                         col_types = cols(.default = col_character()),
                         bucket = "projet-funathon",
                         opts = list("region" = "")) %>% clean_names()


lib_group_cult <- lib_cult %>% 
  select(code_groupe_culture, libelle_groupe_culture) %>% 
  distinct(code_groupe_culture, .keep_all=T)

lib_group_cult %>% kable()
code_groupe_culture libelle_groupe_culture
1 Blé tendre
2 Maïs grain et ensilage
3 Orge
4 Autres céréales
5 Colza
6 Tournesol
7 Autres oléagineux
8 Protéagineux
9 Plantes à fibres
11 Gel (surfaces gelées sans production)
14 Riz
15 Légumineuses à grains
16 Fourrage
17 Estives et landes
18 Prairies permanentes
19 Prairies temporaires
20 Vergers
21 Vignes
22 Fruits à coque
23 Oliviers
24 Autres cultures industrielles
25 Légumes ou fleurs
26 Canne à sucre
28 Divers
Solution
voir le code
#' Crée la table à afficher sur l'application grâce à des calculs sur les
#' données requêtées depuis PostgreSQL.
#' 
#' @param sf Données spatiales.
#' @returns data.frame à afficher.
compute_stats <- function(sf) {
  df <- sf %>% 
    st_drop_geometry() %>%
    count(code_group, name = "parcelles_grp") %>%
    add_tally(parcelles_grp, name = "parcelles_tot") %>%
    mutate(pct_parcelles = round(100*parcelles_grp/parcelles_tot, 1)) %>%
    select(-parcelles_tot) %>%
    cbind(
      # Comptage des surfaces
      sf %>% 
        st_drop_geometry() %>%
        count(code_group, wt = surf_parc, name = "surface_grp") %>% 
        add_tally(surface_grp, name = "surface_tot") %>% 
        mutate(surface_pct = round(100*surface_grp/surface_tot, 1)) %>%
        select(-surface_tot) %>%
        select(surface_grp, surface_pct)
      ) %>% 
    left_join(lib_group_cult, by = c("code_group" = "code_groupe_culture")) %>% 
    select(code_group, libelle_groupe_culture, everything()) %>% 
    arrange(desc(surface_grp)) %>% 
    adorn_totals() %>% 
    mutate(mean_surface = round(surface_grp/parcelles_grp, 1))

  return(
    df %>%
      select(-code_group) %>%
      setNames(
        c(
          "Groupe de cultures",
          "Nombre de parcelles",
          "Pourcentage de parcelles",
          "Surface (ha)",
          "Surface (%)",
          "Surface moyenne d'une parcelle (ha)"))
  )
}

2.4.4 plot_surroundings

La fonction plot_surroundings prend en entrée une carte leaflet ainsi qu’un objet sf récupéré grâce à query_db et renvoie une carte où on a rajouté une couche avec les polygones correspondant aux parcelles. Elle est donné ci-dessous. Essayez de comprendre à quoi correspondent les différents arguments de la fonction addPolygons.

# Création d'une palette de couleurs associée au groupe de culture
pal <- brewer.pal(12, "Paired")
pal <- colorRampPalette(pal)(24)
factpal <- colorFactor(pal, lib_group_cult$code_groupe_culture)

#' Rajoute les données d'un objet `sf` sous forme de polygones à une
#' carte `leaflet`.
#' 
#' @param leaflet_proxy Carte.
#' @param sf Données spatiales.
#' @returns Carte enrichie.
plot_surroundings <- function(leaflet_proxy, sf) {
  # Transformation de la projection (WGS 84)
  sf <- sf %>% st_transform(4326)
  
  # Ajout des libellés des cultures
  sf <- sf %>% 
    left_join(lib_cult %>% select(-code_groupe_culture), by = c("code_cultu" = "code_culture")) 
  
  # Création des labels à afficher au passage de la souris sur la carte.
  labels <- sprintf("<strong>Identifiant de la parcelle : </strong>%s<br/>
                    <strong>Groupe culture : </strong>%s<br/>
                    <strong>Culture : </strong>%s<br/>
                    <strong>Surface (ha) : </strong>%s<br/>
                    <strong>Département : </strong>%s<br/>
                    <strong>Commune : </strong>%s<br/>",
                    sf$id_parcel,
                    sf$libelle_groupe_culture,
                    sf$libelle_culture,
                    sf$surf_parc,
                    sf$insee_dep,
                    sf$nom_com) %>%
    lapply(htmltools::HTML)

  return(
    leaflet_proxy %>%
    addPolygons(
      data = sf,
      fillColor = ~factpal(code_group),
      weight = 2,
      opacity = 1,
      color = "#ffd966",
      dashArray = "3",
      fillOpacity = 0.5,
      highlight = highlightOptions(
        weight = 5,
        color = "#A40000",
        dashArray = "",
        fillOpacity = 0.0,
        bringToFront = TRUE),
      label = labels,
      labelOptions = labelOptions(
        style = list("font-weight" = "normal", padding = "3px 8px"),
        textsize = "15px",
        direction = "auto",
        encoding="UTF-8"))
  )
}

2.5 Lancement de l’application

L’application RShiny se lance localement grâce à la commande shiny::runApp("my-app"). Vérifiez que tout fonctionne bien ! Une application fonctionnelle se trouve dans le répertoire app si besoin.