Rechercher dans le blog

Un mardi, une appli #25 : Breath of the Arctic

C'est la suite du #30DayMapChallenge, et la deuxième carte que je devais réaliser est celle du jour 12, sur le thème space and time (temps et espace).
Je trouve que la manière organique dont l'occupation de l'espace évolue avec le temps est fascinante, que ce soit sur des périodes de dizaines, centaines, milliers, millions d'années ou avec le rythme des saisons, sous l'influence de l'anthropisation ou au gré d'événements naturels, tous ces facteurs étant intimement liés.
Pour l'Arctique, cette évolution bien sûr dramatique. Mais en observant ses variations d'étendue, je ne peux m'empêcher de trouver une certaine poésie à cette image d'un cœur qui bat au fil des saisons mais qui s'affaiblit d'année en année.
Ou, pour pouvoir faire référence au titre d'un des épisodes d'un célèbre jeu vidéo où un type en tunique verte détruit des poteries pour sauver la princesse, nous pouvons aussi utiliser l'image d'un poumon qui respire.  

L'application

Et voici l'application que toutes ces réflexions m'ont inspiré : Breath of the Arctic. N'hésitez pas à cliquer sur le lien pour la consulter en pleine page pour un meilleur confort visuel.
Cette application en projection polaire représente les variations d'étendue de l'Arctique, c'est-à-dire des parties de l'océan ayant gelé pour former une "mer de glace", mois par mois de la fin de l'année 1978 à nos jours. La ligne discontinue blanche montre la superficie maximale enregistrée en mars 1979. 
Les données proviennent du National Snow and Ice Data Center (NSIDC) et sont accessibles depuis le Living Atlas d'ArcGIS

Le code

Nous allons maintenant regarder ensemble le code permettant de faire fonctionner l'application. Celle-ci a été crée grâce à l'API JavaScript d'ArcGIS. Comme d'habitude, vous pouvez recréer cette application de votre côté grâce au tutoriel, mais également retrouver le code complet sur mon Github ou le tester interactivement avec ce Codepen. Si vous sentez que le niveau de ce tutoriel est un peu trop élevé pour vous et que les notions sont expliquées trop rapidement, je vous conseille d'explorer cette série qui prend le temps d'expliquer pas à pas les notions fondamentales. Cela étant dit, c'est parti pour le code !

Le code HTML est extrêmement 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 balise <div id = "full"> qui encapsule toutes mes autres balises, une balise <div id = "dayeDisplay> qui accueillera la date d'enregistrement de la donnée et une balise  <viewDiv> qui contiendra la vue de ma carte. 
<html lang="en"> <!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
  <title>
    Breath of the Arctic
  </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>
  <script type="module" src="https://js.arcgis.com/calcite-components/2.1.0/calcite.esm.js"></script>
</head>

<body >

  <div id="full">

    <div id="dateDisplay">
      <h3>-/-</h3>
    </div>
    <div id="viewDiv"></div>
  </div>

</body>

</html>
Côté CSS, je m'assure simplement que la taille de la <viewDiv> prendra l'entièreté de l'écran et que la date s'affichera avec un style sympa quelle que soit la taille de l'écran.
html,
body,
#viewDiv {
  padding: 0;
  margin: 0;
  height: 100%;
  width: 100%;
  position: absolute;
  background-color: "#212526";
}

h3 {
  z-index: 200;
  padding-top: 1%;
  font-weight: normal;
  font-size: 170%;
  font-family: "Avenir Next", "Avenir", "Helvetica Neue", sans-serif;
  position: absolute;
  z-index: 9;
  text-align: center;
  width: 100%;
  height: 10%;
  margin: auto;
  user-select: none;
}

#dateDisplay {
  z-index: 150;
  font-weight: normal;
  position: absolute;
  font-size: 120%;
  font-family: "Avenir Next", "Avenir", "Helvetica Neue", sans-serif;
  position: absolute;
  user-select: none;
  padding-top: 1.6%;
}
@media screen and (min-width: 500px) {
  #dateDisplay {
    right: 1%;
  }
}
@media screen and (max-width: 500px) {
  #dateDisplay {
    width: 100%;
    text-align: center;
    top: 5%;
  }
}
Il est maintenant temps de faire appel à l'API JavaScript pour insérer notre carte !  Comme d'habitude, je commence par charger les modules qui me seront utiles dans le require (ici, WebMap, MapView, FeatureLayer, GraphicsLayer et Graphic), 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/MapView",
"esri/layers/FeatureLayer",
"esri/layers/GraphicsLayer",
"esri/Graphic"
], function (WebMap, MapView, FeatureLayer, GraphicsLayer, Graphic) { //le reste du code ira ici });
La première étape consiste ensuite à appeler une WebMap à l'intérieur de mon application. Contrairement à une simple Map qui doit être construite de zéro dans le code, la WebMap va se baser sur une carte construite grâce au MapViewer dans ArcGIS Online ou ArcGIS Enterprise. Cette approche permet de faire gagner un temps considérable de développement, en étant capable de configurer les couches et leur symbologie, le fond de carte, le système de coordonnées, mais aussi d'éventuels géosignets etc. sans aucune ligne de code. Si la carte est mise à jour, cela se répercutera également sur l'application sans avoir à maintenir le code. Ici, j'ai donc préparé ma carte dans le MapViewer avec 4 couches dont j'ai configuré la symbologie : les pays du monde pour le fond de carte, l'étendue maximale enregistrée pour l'Arctique (que vous voyez apparaître en pointillés blancs), la couche contenant tous les enregistrements d'étendue de l'Arctique depuis 1978 (qui pour l'instant est cachée mais dont nous nous servirons dans le code), et une couche de dessin qui est simplement un cercle blanc dont j'ai modifié la symbologie et qui me permet de créer l'effet de halo que vous voyez autour du pôle Nord. 
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=360827c4ab4f4aad85b771bd453a7254). Vous pouvez directement utiliser la même carte que moi, car je l'ai partagée en public.
const map = new WebMap({
  portalItem: {
    id: "360827c4ab4f4aad85b771bd453a7254"
  }
});
Je crée ensuite une vue sur ma carte grâce à la MapView, en indiquant le centre (en long/lat) ainsi que le niveau de zoom. Ici, je rajoute également des contraintes afin d'empêcher l’utilisateur final de modifier le niveau de zoom. Je retire également les éléments d'interface utilisateur (UI) intégrés par défaut à la MapView pour épurer l'application.
const view = new MapView({
  container: "viewDiv",
  map: map,
  center: [2277430.5171051705, 2076536.5900727776],
  zoom: 4,
  constraints: {
    maxZoom: 4,
    minZoom: 4
  }
});
view.ui.remove(["zoom"]);
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 l'historique d'étendue de l'Arctique. Le calque étant en 3ème position de la carte, je le retrouve à l'index 2 en accédant aux layers de la map et je le stocke à l'intérieur de la variable arcticExtentLayer.  
  const arcticExtentLayer = map.layers.items[2];
  arcticExtentLayer.visible = false;
Je crée maintenant une couche graphique (GraphicsLayer) que j'ajoute à la carte. Pour l'instant, cette couche est vide mais elle me servira à afficher dynamiquement une à une les différentes étendues de l'Arctique depuis 1978.
  const graphicsLayer = new GraphicsLayer();
  map.add(graphicsLayer);
Avec la méthode queryFeatures(), je viens interroger la couche arcticExtentLayer. La query va interroger la couche et en retourne la géométrie et les attributs de chacune des entités sous la forme d'un tableau de Graphics contenu dans un FeatureSet, auquel on accède grâce à la méthode .then(). Pour la suite, nous travaillerons à l'intérieur du .then() jusqu'à la fin du tutoriel.
arcticExtentLayer.queryFeatures().then((results) => { //le reste du code ira ici });
Chaque objet  est ensuite accessible en parourant results.features grâce à la méthode .map(). Nous allons retourner un Graphic pour chacune des entités de la couche, et les stocker dans un tableau que nous appelons features. Pour construire les Graphics, nous renseignons les propriétés geometry et attributes en nous servant des informations renvoyées par la query, et nous définissons à la main la symbologie, ici un polygone bleu clair avec un contour bleu vif. 
const features = results.features.map((feature) => {
  return new Graphic({
    geometry: feature.geometry,
    attributes: feature.attributes,
    symbol: {
      type: "simple-fill",
      color: [203, 242, 242, 0.3],
      outline: { color: [133, 242, 242], width: 1 }
    }
  });
});
Nous allons enfin créer une fonction qui nous permet d'afficher un à un les graphiques créés ci-dessus. Pour cela, nous initialisons une variable index à 0 qui nous permettra d'appeler un à un les graphiques stockés dans le tableau features. Nous créons ensuite la fonction displayFeature(). Celle-ci commence par retirer tous les graphiques potentiellement présents à l'écran pour s'assurer que nous affichons les étendues de l'Arctique une par une. Nous appelons ensuite le graphique correspondant à l'index actuellement parcouru (celui-ci s'incrémente de 1 à chaque passage dans la fonction afin de parcourir tous les graphiques un par un), et nous ajoutons ce graphique à la carte. 
Afin d'indiquer à l'utilisateur de quand date l'étendue actuellement affichée à l'écran, la fonction récupère également les informations de mois et d'année contenues dans les attributs du graphique et les injecte dans le HTML. Enfin, afin de souligner les changements d'années, les années paires et impaires sont affichées dans des couleurs différentes.
let index = 0;
function displayFeature() {
  graphicsLayer.removeAll();  // Supprime les graphiques précédents
  const currentFeature = features[index]; // Récupère la fonctionnalité actuelle
  graphicsLayer.add(currentFeature); // Ajoute le graphique à la couche

  // Extraction et affichage des informations temporelles
  const month = currentFeature.attributes["Rec_Month"];
  const year = currentFeature.attributes["Rec_Year"];
  dateDisplay.innerHTML = `${month}/${year}`;

  // Modification de la couleur de la date en fonction de l'année (paire ou impaire)
  if (year % 2 === 0) {
    dateDisplay.style.color = "rgb(108, 166, 166)"; // Années paires
  } else {
    dateDisplay.style.color = "rgb(150, 212, 212)"; // Années impaires
  }

  // Incrémentation de l'index pour passer à la fonctionnalité suivante
  index = (index + 1) % features.length;
}
Afin, nous appelons la fonction tous les 100 millisecondes setInterval(). Cela crée une animation en affichant une fonctionnalité à la fois dans graphicsLayer, donnant l’impression de visualiser les données géospatiales de manière continue.
setInterval(displayFeature, 100);
Voici le code JS complet que vous devriez obtenir : 
require([
  "esri/WebMap",
  "esri/views/MapView",
  "esri/layers/FeatureLayer",
  "esri/layers/GraphicsLayer",
  "esri/Graphic"
], function (WebMap, MapView, FeatureLayer, GraphicsLayer, Graphic) {
  const map = new WebMap({
    portalItem: {
      id: "360827c4ab4f4aad85b771bd453a7254"
    }
  });

  const view = new MapView({
    container: "viewDiv",
    map: map,
    center: [2277430.5171051705, 2076536.5900727776],
    zoom: 4,
    constraints: {
      maxZoom: 4,
      minZoom: 4
    }
  });
  view.ui.remove(["zoom"]);

  map.when(() => {
    const arcticExtentLayer = map.layers.items[2];
    arcticExtentLayer.visible = false;
    const graphicsLayer = new GraphicsLayer();
    map.add(graphicsLayer);

    arcticExtentLayer.queryFeatures().then((results) => {
      const features = results.features.map((feature) => {
        return new Graphic({
          geometry: feature.geometry,
          attributes: feature.attributes,
          symbol: {
            type: "simple-fill",
            color: [203, 242, 242, 0.3],
            outline: { color: [133, 242, 242], width: 1 }
          }
        });
      });

      let index = 0;
      let previousYear = null;
      function displayFeature() {
        graphicsLayer.removeAll();
        const currentFeature = features[index];
        graphicsLayer.add(currentFeature);

        // Récupérez les informations de date et mettez à jour l'affichage
        const month = currentFeature.attributes["Rec_Month"];
        const year = currentFeature.attributes["Rec_Year"];
        dateDisplay.innerHTML = `${month}/${year}`;
        if (year % 2 === 0) {
          dateDisplay.style.color = "rgb(108, 166, 166)"; // Années paires en bleu foncé
        } else {
          dateDisplay.style.color = "rgb(150, 212, 212)"; // Années impaires en bleu clair
        }

        index = (index + 1) % features.length;
      }

      setInterval(displayFeature, 100); // Mise à jour toutes les 100 ms
    });
  });
});
La partie cartographique est terminée ! De mon côté, j'ai enrichi l'application grâce à un loader en utilisant des éléments de Calcite, comme nous l'avions vu dans ce tutoriel. J'ai également rajouté un titre, des crédits et une fenêtre informative quand l'utilisateur clique sur "En savoir +" en ajoutant un peu de HTML et de CSS.  Nous verrons cela en détails d'ici la fin de l'année dans un tutoriel consacré aux composants. En attendant, n'hésitez à nouveau pas à consulter le code complet sur Github ou le sur Codepen afin d'essayer d'en faire de même ! 
 
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 carte d'isohypses pour représenter les concentrations des photos que j'ai prises dans ma ville.

Aucun commentaire:

Enregistrer un commentaire