Rechercher dans le blog

Un mardi, une appli #22 : Le Tour du Monde en 80 Jours

Bonjour à tous !

En plus de coder des applis, je lis parfois des livres. Cette période de fin d'année rime avec le plaisir de retrouver certains classiques. Vous connaissez certainement celui dont je vais vous parler aujourd'hui, faisant la part belle à la géographie, où Phileas Fogg et son comparse Jean Passepartout s'embarquent dans un un tour du monde frénétique suite à un pari entre membres du Reform-Club. Je veux bien sûr parler du célèbre roman "Le Tour du Monde en 80 Jours" de Jules Verne, sorti il y a déjà 150 ans (ce qui, a priori, correspond au temps nécessaire pour parcourir environ 685 tours du monde).

Évidemment, je ne vous propose pas aujourd'hui une revue littéraire, mais plutôt de coder ensemble une application nous permettant de retracer cette grandiose aventure autour du globe. Faites vos valises, c'est parti !

L'appli

Sur internet, les cartes retraçant le voyage de Phileas Fogg que j'ai pu trouver sont en 2D. 

Cependant, une application 3D me semble mieux retranscrire cette notion de "tour" du monde et permet également de mieux appréhender la continuité de son parcours. Je suis donc partie sur la conception d'une application 3D en utilisant l'API JavaScript d'ArcGIS, avec un style rétro/vintage (permis notamment par le fond de carte utilisé) pour mieux se replonger 150 ans en arrière. Cette carte comporte bien sûr le tracé de l'itinéraire du héros, donnée clé que je voulais mettre en avant. Ce tracé est divisé entre les différents tronçons que Phileas a parcouru durant son aventure, que l'utilisateur peut bien sûr interroger en cliquant dessus. 

Lors de l'interrogation de la couche, vous pouvez voir en action l'une des nouveautés de la version 4.25 de l'API, à savoir la possibilité d'afficher des enregistrements associés dans une fenêtre contextuelle, qui permet de mettre en avant les péripéties vécues par les personnages sur chacun des tronçons de route.

J'ai longuement hésité avec un application 2D utilisant une projection polaire, je vous laisse ci-dessous un aperçu de ce à quoi cela aurait pu ressembler :


 

Le code

L’application se base sur l’API JavaScript d’ArcGIS, et vous pouvez en retrouver le code commenté sur mon Github.

 

Les données 

Pour cette application, j'ai dû créer la plupart des données car elles n'étaient pas disponibles sur le web dans un format exploitable. J'ai utilisé ArcGIS Pro pour digitaliser le tracé du trajet à partir de la carte proposée par De Neuville et Benett, les illustrateurs du roman. J'ai fait de même pour les villes étapes du voyage. J'ai également ajouté deux tables non géométriques, l'une pour les péripéties rencontrées par les héros et l'autre pour les moyens de transport utilisés, que j'ai renseignées avec les informations données par le roman. J'ai ensuite créé des classes de relation entre ces tables non géométriques et la couche du tracé qui nous permettront d'utiliser la fonctionnalité d'affichage des enregistrements associés. Pour le fond de carte, j'ai choisi la basemap vectorielle "Modern Antique Map" d'Esri, accessible sur ArcGIS Pro, ArcGIS Online et bien sûr via l'API Javascript. J'ai enfin publié ma scène web au sein de mon organisation ArcGIS Online, et en ai fait un partage public. N'hésitez pas à cliquer sur chacun des liens si vous souhaitez accéder aux différents services de couches pour les tester.

Ajout de la scène web à la vue

Contrairement à l'appli précédente, où nous avions construit une scène de zéro et y avions ajouté les données, nous pouvons cette fois-ci directement utiliser la scène web publiée sur mon portail grâce à son identifiant (la suite de chiffres et de lettres que vous trouverez à la fin de l'url de la scène.


const scene = new WebScene({
	portalItem: {
   		id: "d3b84f82ac5e410b8c6f02f5f4d37248" // ID de la scène web sur arcgis.com
   	}
});

Bien sûr, il reste possible d'ajouter et de modifier les données de  la scène web. J'ajoute donc mes deux tables non géométriques à la scène et configure leurs fenêtres contextuelles.

        
//// Ajout des tables      
//L'ajout des tables à la scène nous permettra d'afficher les enregistrements associés dans les fenêtres contextuelles

//Table des péripéties
const textElementPeripeties = new TextContent();
textElementPeripeties.text = "{Description}"
const peripetiesTable = new FeatureLayer({
	url: "https://services.arcgis.com/d3voDfTFbHOCRwVR/ArcGIS/rest/services/CTM_22_TDM80J_Scene_WFL1/FeatureServer/5", //lien vers le service web
    title: "Péripéties",
    popupTemplate: { //fenêtre contextuelle
    	title: "{Titre}",
    	content: [textElementPeripeties]
    }
});

//Table des moyens de transport
const textElementTransports = new TextContent();
textElementTransports.text = "{Precisions}"
const transportsTable = new FeatureLayer({
	url: "https://services.arcgis.com/d3voDfTFbHOCRwVR/ArcGIS/rest/services/CTM_22_TDM80J_Scene_WFL1/FeatureServer/4",
    title: "Moyens de transport",
    popupTemplate: {
    	content: [textElementTransports]
    }
});
peripetiesTable.load().then(() => {
scene.tables.addMany([peripetiesTable, transportsTable]); //ajout des tables à la scène
});

 

Enfin, je transmets la scène à une vue. Sur cette vue, je configure les options de surlignage pour que les entités sélectionnées apparaissent en rouge (au lieu du bleu fluo habituel). Je change aussi l'environnement pour un fond beige clair uni, le fond étoilé et l'atmosphère par défaut donnant un mon goût un côté trop futuriste à la scène. C'est également dans l'environnement que je configure un éclairage artificiel.


const view = new SceneView({
     map: scene, //scène web référencée plus tôt
     alphaCompositingEnabled: true,
     container: "viewDiv",
     scale: 52000000,
     center: [0, 40],
     highlightOptions: { //style des entités sélectionnées
            color: [207, 71, 48],
            haloColor: "white",
            haloOpacity: 0.3,
            fillOpacity: 1,
            shadowColor: "black",
            shadowOpacity: 0.5
          },
      environment: { //configuration de l'environnement (atmosphère, fond, étoiles)
            background: {
              type: "color",
              color: [245, 241, 235, 1]
            },
            starsEnabled: false,
            atmosphereEnabled: false,
            lighting: {
              type: "virtual" //l'éclairage virtuel suit toujours la caméra 
            }
       },
       popup: { //épinglera automatiquement les fenêtres contextuelles en haut à droite
            dockEnabled: true,
            dockOptions: {
              position: "top-right",
              breakpoint: false,
              buttonEnabled: false
            },
       }
});

 

Configuration de la couche d'itinéraire

Je vais ensuite configurer la couche contenant l'itinéraire de Phileas. J'accède à celle-ci grâce à la méthode getItemAt() à laquelle je donne en argument l'index de ma couche dans la scène.

const removedRoute = scene.layers.getItemAt(0);

J'accède ensuite à la symbologie de cette couche pour y appliquer un style de ligne 3D et un effet pointillé rouge.


route.renderer = { //symbologie
      type: "simple",
      symbol: {
      	type: "line-3d", 
        symbolLayers: [{
            type: "line", 
            size: 2, 
            material: {
                  color: [207, 71, 48]
            },
            cap: "round",
            join: "round",
            pattern: { 
                  type: "style",
                  style: "dash"
            }
         }]
      }
 };

Enfin, je configure les fenêtres contextuelles. Le contenu de type "relationship", introduit avec la version 4.25 de l'API, me permet d'afficher dans la fenêtre contextuelle les informations des enregistrements associés via une classe de relation. 


  const textElementRoutes = new TextContent();
  textElementRoutes.text = "L'étape {OBJECTID} a duré {Duree} jours. Les héros sont partis de {Depart} pour arriver à {Arrivee}"
  route.popupTemplate = { //Popups
    title: "{Titre}",
    outFields: ["*"],
    content: [textElementRoutes,
      {
        type: "relationship", //Enregistrements associés à l'itinéraire #1 (moyens de transort)
        relationshipId: 1, //ID de la relation que vous pouvez retrouver dans la définition du service
        description: "Durant cette étape, les héros ont emprunté ces moyens de transport :",
        displayCount: 3, //Nombre d'enregistrements associés à afficher
        title: "Moyens de transport",
        orderByFields: { //Tri dans l'ordre alphabétique
          field: "Moyen_Transport",
          order: "asc"
        }
      },
      {
        type: "relationship", //Enregistrements associés à l'itinéraire #2 (péripéties)
        relationshipId: 0,
        description: "Voici les péripéties rencontrées par Phileas et ses compagnons lors de l'étape {OBJECTID} :",
        displayCount: 5,
        title: "Péripéties",
        orderByFields: { //Tri par OBJECTID croissant
          field: "OBJECTID",
          order: "asc"
        }
      }
    ]
  };

J'ai ici deux tables dont les informations vont apparaître lorsque j'interrogerai la couche de trajet : les moyens de transport empruntés sur chaque tronçon ainsi que les péripéties rencontrées sur le tronçon en question.


Configuration de la couche de villes

De la même manière, j'accède à la couche des villes. J'en modifie le contenu de la fenêtre attributaire, notamment pour afficher des images de chacune des villes.

const villes = scene.layers.getItemAt(0);
villes.outFields = ["*"];

/////popup
const attachmentsElement = new AttachmentsContent({
    displayType: "preview",
});

villes.popupTemplate = {
title: "{Nom}",    
content : [attachmentsElement]
};   

Enfin, je modifie la symbologie. J'ai utilisé ici un icône au format png hébergé sur mon Github pour représenter les villes.  

villes.elevationInfo.mode = "relative-to-ground"; 

villes.renderer = {
    type: "simple",  // autocasts as new SimpleRenderer()
    symbol: {
        type: "point-3d",  // autocasts as new SimpleFillSymbol()
        symbolLayers: [{
            type: "icon",  // autocasts as new IconSymbol3DLayer()
            resource: {href : "https://raw.githubusercontent.com/JapaLenos/JS-API/main/Le-Tour-du-Monde-en-80-Jours/assets/pin.png"},
            size : 25,
            anchor: "relative",
            anchorPosition: {
                x: 0,
                y: 0.4
            }
        }]
    }
};  

Zoom sur les entités sélectionnées et autres ajouts esthétiques

Vous remarquerez dans la suite du code deux blocs de code quasiment identiques, l'un pour zoomer sur les villes lors de la sélection par l'utilisateur, et l'autre pour faire de même sur les tronçons de route. Ce code enregistre les clics sur les couches, d'identifier le type de couche interrogé et de retourner l'ObjectID de l'entité sélectionnée. La méthode goTo() permet ensuite de déplacer la vue sur l'étendue de l'entité renvoyée par la requête. 

  
view.whenLayerView(villes).then((layerView) => {
    // enregistre un clic sur la vue
    view.on("click", (event) => {
      // utilise hitTest pour voir si l'utilisateur a cliqué sur un graphique
      view.hitTest(event).then((response) => {
          console.log("response",response.results[0])
        // vérifie si un graphique est retourné par le hitTest et vérifie que c'est un point (pour ne pas confondre les villes avec les itinéraires)
        if (response.results[0] && response.results[0].graphic && response.results[0].graphic.layer.geometryType=="point") {
            console.log("rep.resultd",response.results[0].graphic.attributes)
            console.log("ICI",response.results[0].graphic.attributes.OBJECTID)
          //La requpete va renvoyer les résultats pour les entités dont l'objectID correspond à celui du graphique sélectionné
          const query = new Query({
            objectIds: [
              response.results[0].graphic.attributes.OBJECTID
            ],
            // indique que la requête doit retourner tous les attributs
            outFields: ["*"]
          });
          // queryExtent() retournera l'étendue 3D de l'entité sélectionnée
          layerView.queryExtent(query).then((result) => {
              console.log("rép.resultd",result)
            view
              .goTo( //goTo permet de se rendre à la nouvelle vue indiquée
                {
                  target: result.extent.expand(2500000),
                  tilt: 10
                },
                {
                  duration: 5000,
                  easing: "out-expo"
                }
              )
              .catch((error) => {
                if (error.name != "AbortError") {
                  console.error(error);
                }
              });
          });
        }
      });
    });
  });  

J'ai ensuite enlevé les widgets ajoutés automatiquement à l'ui pour épurer l'interface.


view.ui.remove("navigation-toggle");
view.ui.remove("zoom");
view.ui.remove("compass");  

Enfin, vous pourrez trouver tout à la fin du code une fonction de rotation faisant tourner le globe dans le sens de l'itinéraire. Vous pouvez la décommenter pour la voir en action (notez cependant qu'elle agit en conflit avec le zoom, et que celui-ci ne fonctionnera donc pas tant que vous n'aurez pas agit une première fois avec l'app si vous activez la fonction de rotation).

Mot de la fin

C'est fini pour cette dernière application du dernier mardi de l'année ! J'espère que vous avez autant apprécié que moi de vous replonger dans les aventures de Phileas à travers cette manière originale de faire (re)découvrir ce voyage. Pour la prochaine édition d'une version un peu plus littéraire d'un mardi une appli, il est possible que Jules Vernes nous permette d'explorer les fonctionnalités d'altitude (ou de profondeur...) qu'offre l'API. 

Pour découvrir les autres fonctionnalités introduites avec la nouvelle version de l'API, n'hésitez pas à consulter l'article des nouveautés de la version 42.5 de l'ArcGIS API for JavaScript. Si vous ne l'avez pas encore consulté, n'hésitez également pas à découvrir l'édition numéro 20 d'un mardi une appli, où nous étions partis sur les toits du monde.

Pour le reste, on se retrouve l'an prochain pour découvrir ensemble les nouveautés à venir, développer de nouvelles applications, tester d'autres fonctionnalités et coder d'autres cartes !

Aucun commentaire:

Enregistrer un commentaire