Rechercher dans le blog

Un mardi, une appli #26 : une manière originale de représenter une densité de points

C'est déjà le dernier jour du #30DayMapChallenge pour lequel je réaliserai une carte, aujourd'hui sur le thème "Mes données" (My Data). Ma seule manière de collecter des données au quotidien, c'est de prendre des photos, et ce d'à peu près tout. A l'heure où j'écris ces lignes, mon téléphone atteint le nombre inquiétant de 14874 photos, pour un total de presque 39 Go de stockage (dont un bon tiers sont sans doute des photos de mon chien, je dois l'admettre).
Évidemment, nos photos en disent très long sur nous. Et parmi les mille et une manières d'en apprendre plus sur quelqu'un à travers ses photos, en voilà une que j'aime bien : afficher les endroits où les photos ont été prises sur une carte, comme par exemple dans cette application réalisée avec l'API JavaScript d'ArcGIS (pour plus de confort visuel, n'hésitez pas à cliquer sur le lien pour la voir en plein écran). 
Comme vous le voyez, je ne me suis pas contentée d'afficher les points, mais ai créé une carte d'élévation fictive en fonction de la densité de points à chaque endroit.
 
Dans cet article, je vous propose de suivre le cheminement de création de l'application, de la préparation des données jusqu'au code (qui est très abordable, promis). De quoi recréer la même chose avec vos propres données !

Préparation des données

Dans un premier temps, j'ai créé ma propre boîte à outil via un script Python en utilisant arcpy dans ArcGIS Pro, qui me permet de parcourir tous les fichiers photos de mon téléphone, et de récupérer les coordonnées XY de chacune des photos via leur fichier EXIF (des métadonnées associées à votre photo lors de la prise de vue) afin de les placer sur une carte. ArcGIS Pro possède déjà un outil Photos géolocalisées vers points qui fonctionne très bien pour faire cela, mais il peut rencontrer des problèmes lorsque les formats de votre jeu de donnée d'entrée ne sont pas lisibles par l'outil (qui prend en charge des jpeg et des tiff), ce qui est souvent le cas sur nos portables où nous pouvons par exemple collecter des gif, des mp4 etc. Dans un article très prochain, nous reviendrons en détail sur la création d'une Toolbox ArcGIS Pro en utilisant arcpy à travers cet exemple. En attendant, vous retrouverez ici un Notebook que vous pouvez utiliser sur vos données pour créer vos propres cartes.
Voici le résultat une fois les photos affichées sur la carte :
En lisant cette carte, je vois énormément de choses : où j'habite et je travaille, où mes parents habitent, où j'ai fait mes études, où je pars en vacances, etc. Elle me permet même de faire revivre des souvenirs, comme une rapide journée en Belgique il y a quelques années que j'avais complètement oubliée.
 
Bon, est-ce qu'on peut faire mieux ? Je pense que oui ! En me baladant sur le Github de la géniale Raluca Nicola, je suis tombée une application représentant des densités de points avec des isohypses en 3D. J'étais curieuse de voir ce que cela pouvait donner sur mes photos. 

C'est parti ! Je choisis dans un premier temps de me concentrer sur une zone géographique restreinte, par exemple le Sud-Ouest de Paris dans lequel je travaille actuellement et je fais pas mal d'activités. J'ai donc extrait les points correspondant à cette zone puis ai suivi cet article pour préparer mes données dans ArcGIS Pro : en appliquant l'outil Densité de Noyau, j'ai obtenu un raster contenant des valeurs plus ou moins élevées selon la densité de points. Pour paramétrer l'outil sur mes propres données, je dois avouer avoir opté pour l'approche très scientifique d'y aller à tâtons jusqu'à obtenir un résultat que je trouvais suffisamment détaillé. Il n'y a de toute façon rien de scientifique à cette application, c'est avant tout pour la beauté de la chose.
Ma densité étant globalement très basse, mon raster était composé uniquement de valeurs décimales. Je suis passée par la calculatrice raster pour le multiplier (dans mon cas par 1000000) obtenir un nouveau raster valeurs comprises entre environ 0 et 455, qui ressemblent plus à des valeurs "d'élévation" desquelles on pourra tirer quelque chose.
Enfin, l'outil Isolignes permet de transformer ce raster en isolignes, c'est à dire des lignes d'égales valeurs. J'ai édité les entités afin de refermer chacune des isolignes (encore une fois, rien de scientifique, tout pour le style). Voici le résultat final :
J'ai ensuite publié la carte contenant les isolignes ainsi que les points représentant chacune des données et les cartes sur mon portail ArcGIS (ArcGIS Online ou Enterprise).
Il est temps de passer au code ✨

Le code

Ici, nous sommes sur une application très basique qui utilise l'API JavaScript d'ArcGIS, pour laquelle j'ai simplement modifié les rendus des couches. Si vous êtes totalement débutant avec l'API, vous pouvez essayer de suivre ce tuto, mais si vous voyez que certaines notions sont expliquées trop rapidement, rendez-vous sur cette série de prise en main de l'API où tout est expliqué pas à pas. Vous pouvez également retrouver le code complet sur mon Github ou le modifier interactivement dans ce CodePen. Cette application n'étant qu'un remaniement de celle de Raluca, vous pouvez aussi consulter directement son code si vous le souhaitez.
 
Comme d'habitude, nous commençons avec un HTML très simple, avec un appel à l'API JavaScript https://js.arcgis.com/4.30/ ainsi qu'à son style https://js.arcgis.com/4.30/esri/themes/dark/main.css, que nous allons utiliser ici en mode sombre (dark). Je référence également ma propre feuille de style et mon propre script que j'ai séparés du HTML. Dans le corps du HTML, j'insère une unique balise <viewDiv> qui contiendra la vue de ma carte. 
<html lang="en">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
  <title>
    France | Élévation
  </title>
  <link rel="stylesheet" href="style/style.css" />


  <link rel="stylesheet" href="https://js.arcgis.com/4.30/esri/themes/dark/main.css" />
  <script src="https://js.arcgis.com/4.30/"></script>
  <script src="app/main.js"></script>

</head>

<body>
    <div id="viewDiv"></div>
</body>

</html>
Côté CSS, je m'assure simplement que la taille de la <viewDiv> prendra l'entièreté de l'écran.
html,
body,

#viewDiv {
  padding: 0;
  margin: 0;
  height: 100%;
  width: 100%;
  position: absolute;
}

Il est maintenant temps de faire appel à l'API JavaScript pour insérer notre carte ! 

Il est maintenant temps de faire appel à l'API JavaScript pour insérer notre carte ! 
Comme d'habitude, je commence par appeler les classes qui me seront utiles dans le require (ici, WebMap, SceneView, FeatureLayer, PointSymbol3D et IconSymbol3DLayer), puis je déclare la fonction qui contiendra toutes les instructions utiles à l'affichage de ma carte et je lui passe en arguments chacune de ces classes dans le même ordre que dans le require.
require([
  "esri/WebMap",
  "esri/views/SceneView",
  "esri/layers/FeatureLayer",
  "esri/renderers/SimpleRenderer",
  "esri/symbols/PointSymbol3D",
  "esri/symbols/IconSymbol3DLayer"
], function (WebMap, SceneView, FeatureLayer, SimpleRenderer,PointSymbol3D,IconSymbol3DLayer) {
//le reste du code ira ici
});
Je vais commencer par appeler ma carte publiée sur mon portail avec la classe WebMap. A la place de construire la carte de zéro avec la classe Map, la classe WebMap me permet d'appeler directement une carte publiée sur le portail, en conservant les configurations en termes de couches, symbologie (si supportée), géosignets, projection, etc.). J'ajoute donc simplement cette WebMap à mon code en indiquant son id, que vous pouvez retrouver facilement dans l'url de la page de l'item. (exemple ici : https://esrifrance.maps.arcgis.com/home/item.html?id=ecab473029a94c9f90904997dd7836d0). Vous pouvez directement utiliser la même carte que moi, car je l'ai partagée en public, mais si vous suivez le tutoriel avec vos propres données, pensez à modifier la valeur d'id.
const map = new WebMap({
  portalItem: {
    id: "ecab473029a94c9f90904997dd7836d0"
  }
});
Je crée ensuite une instance de SceneView, qui va me permettre de créer une vue 3D sur une carte. 
  const view = new SceneView({
    container: "viewDiv",
    map: map,
    camera: {
      position: {
        longitude: 2.219092028824691,
        latitude: 48.78941188838513,
        z: 2771.364869683981
      },
      heading: 17.062531917143495,
      tilt: 60.592038831044185
    },
    viewingMode: "local",
    environment: {
      background: {
        type: "color",
        color: "#212526"
      },
      atmosphereEnabled: false,
      starsEnabled: false
    }
  });
Je commence par indiquer le container, la balise de mon HTML dans laquelle la carte sera affichée, ici "viewDiv" comme nous l'avons défini au tout début du tutoriel. J'indique également que la carte (map) sur laquelle je génère la vue est la carte créée juste ci-dessus. 
Je dois ensuite placer la camera, qui donnera l'angle initial avec lequel je dois virtuellement filmer ma carte. Pour cela, je définis une orientation par rapport au Nord (heading), une inclinaison (tilt), la position constituée de la longitude, de la latitude et de l'élévation (z) ainsi qu'une référence spatiale (spatialReference). 
Je peux également déterminer qu'il s'agit d'une scène locale grâce au viewingMode, et modifier l'environment en choisissant une couleur d'arrière-plan (background) ainsi qu'en supprimant l'atmosphère et les étoiles présentes par défaut.
Je retire également les widgets de l'interface utilisateurs présents par défaut afin d'épurer l'application au maximum.
  view.ui.remove([ "compass", "zoom","navigation-toggle"]);
Pour la suite, je veux m'assurer que la carte et ses couches soient entièrement chargées avant de les manipuler. J'utilise pour cela une instruction map.when(() => { ... }) à l'intérieur de laquelle je placerai toute la suite de mon code. 
J'accède ensuite à la couche contenant les isolignes. Le calque étant en 2ème position de la carte, je le retrouve à l'index 1 en accédant aux layers de la map et je le stocke à l'intérieur de la variable isohypses.  
  const isohypses = map.layers.items[1];
Attention, votre couche n'est peut-être pas dans la même position à l'intérieur de votre carte, pensez à vérifier et à adapter l'index en fonction.
Afin de décaler ces isolignes sur l'axe Z, j'accède à la propriété elevationInfo de la couche. Le mode d'élévation choisi est "relative-to-ground", c'est-à-dire que la hauteur d'affichage sera calculée en prenant le sol comme référence pour le zéro. Pour choisir la valeur de Z affichée, nous utilisons la propriété featureExpressionInfo et indiquons que celle-ci sera calculée à partir de l'attribut Contour de chacune des entités. Lors de la création des isolignes dans ArcGIS Pro, l'outil a automatiquement créé ce champ Contour dans lequel il a stocké les valeurs de chacune des isolignes.
    const isohypses = map.layers.items[1];
    (isohypses.elevationInfo = {
      mode: "relative-to-ground",
      featureExpressionInfo: {
        expression: "$feature.Contour"
      }
    }),
Vous devirez maintenant voir vos lignes décoller du sol. Cependant pour l'instant, elles ne sont pas très sexys. Nous allons maintenant appliquer un gradient de couleur, pour partir d'un rose foncé pour les lignes les plus basses en allant d'un rose foncé pour les lignes de faible altitude jusqu'à un rose très clair pour les lignes les plus hautes. Nous allons pour cela accéder au renderer de la couche. Ici, nous allons utiliser un rendu simple (SimpleRenderer) associé à des variables visuelles (visualVariables).  
      (isohypses.renderer = new SimpleRenderer({
        symbol: {
          type: "line-3d",
          symbolLayers: [
            {
              type: "line",
              size: 1
            }
          ]
        },
        visualVariables: [
          {
            type: "color",
            field: "Contour",
            stops: [
              {
                value: 25,
                color: "#7A0177"
              },
              {
                value: 200,
                color: "#F768A1"
              },
              {
                value: 400,
                color: "#FDE0DD"
              }
            ]
          }
        ]
      }));
Nous construisons un SimpleRenderer, pour lequel nous commençons par  définir le symbole utilisé. Puisque je travaille en 3D sur une donnée linéaire, le type de symbole est  "line-3d". Je peux ensuite indiquer le symbolLayers, permettant de définir la manière dont les objets seront visualisés. Ici, nous définirons le type sur "line" nous pouvons également jouer sur sa taille avec le paramètre size.
Enfin, pour faire varier la couleur "l'altitude" moyenne de l'hexagone, nous allons tirer parti des visualVariables. Ici, une ColorVariable pour faire varier la couleur. Indiquons cela en paramétrant le type sur "color", puis définissons le champ (field) selon lequel la couleur devra varier, ici "Contour. Nous pouvons ensuite indiquer les stops, qui sont un array contenant des valeurs clés auxquelles nous attribuons des valeurs précises. Ici par exemple, nous définissons 3 valeurs de contour qui prendront une couleur particulière que nous passons avec sa valeur hexadécimale (il est également possible d'utiliser du rgb). Entre ces valeurs, l'API va déterminer automatiquement une rampe de couleurs pour attribuer des couleurs à chacune des lignes.N'hésitez pas à tester d'autres couleurs si vous le souhaitez.
 
Pour compléter l'application, nous allons afficher au sol les points de collecte des photos. Nous accédons à la couche, puis indiquons que le mode d'élévation sera sur le sol.
    const photos = map.layers.items[2];
    (photos.elevationInfo = {
      mode: "on-the-ground"
    }),
Enfin, nous allons également modifier le rendu de la couche. A nouveau, nous allons construire un  un SimpleRenderer, pour lequel nous commençons toujours par  définir le symbole utilisé. Cette fois-ci, puisque nous travaillons sur des points, nous allons utiliser des PointSymbol3D. Nous allons renseigner sa propriété symbolLayers, en utilisant un IconSymbol3DLayer pour avoir un rendu plat plutôt que volumétrique. Nous indiquons ensuite quelle ressource (resource) est utilisée pour cette icone, ici nous utiliserons une forme de type cercle, quel matériel (material) de rendu nous souhaitons utiliser, ici  un simple aplat de couleur, ainsi que la taille (size) des points.
      (photos.renderer = new SimpleRenderer({
        symbol: new PointSymbol3D({
          symbolLayers: [
            new IconSymbol3DLayer({
              resource: {
                primitive: "circle"
              },
              material: {
                color: "#DD3497"
              },
              size: 3
            })
          ]
        })
      }));
Voilà, la partie cartographique de l'application est prête ! Vous pouvez désormais enrichir l'UI avec par exemple un titre, des crédits, un loader, etc. Pour cela, vous pouvez prendre inspiration sur mon code (dans Github ou CodePen), ou alors vous amuser par vous-même ! Pour ma part, j'utilise très souvent Calcite pour cela.

Si cet article vous a plus, n'hésitez pas à retrouver les tutoriels de mes deux autres créations cartographiques pour ce challenge : la carte d'élévation de la France en hexagones, et une animation sur les variations d'étendues de l'Arctique.

Aucun commentaire:

Enregistrer un commentaire