I. Introduction

Monsieur Leguilvinec, souhaite connaître le meilleur emplacement géographique pour vivre dans la banlieue Sud-Ouest de Nantes. Il nous propose une liste de villes :

  • Bouaye, parce que c'est un joli village proche de la réserve naturelle de Grand-Lieu ;
  • Saint-Mars-de-Coutais, parce que sa mère y vit ;
  • Saint-Aignan-Grandlieu, parce que ce n'est vraiment pas chère à cause de l'aéroport ;
  • Les Sonnières, parce que c'est vraiment tout près du travail.

Ces villes seront nos points de départ. Les points d'arrivée sont :

  • Le CHU, parce qu'il y travaille ;
  • Saint-Mars-de-Coutais, parce que sa mère y vit ;
  • Le point géographique 47.123819 -1.66783, parce que c'est là que se trouve sa barque ;
  • Saint-Herblain, parce que c'est là que se trouve son supermarché préféré.

Le code va utiliser le service JSON (JavaScript Object Notation) de la Google Distance Matrix API. Ce service permet de connaître la distance et le temps de parcours entre deux points géographiques en voiture, à pieds ou à vélo.

II. Création du model

Les données renvoyées par un service JSON sont au format texte. Elles ne sont donc pas interprétables directement par le code. Pour résoudre cette difficulté, le plus simple est de créer des objets .Net et de désérialiser la chaîne JSON obtenue grâce aux fonctionnalités du Framework 4.

Pour ceux qui, comme moi, ne lisent pas le JSON dans le texte et qui sont plus habitués au XML, c'est là que vous aurez le plus de difficultés. Le reste du code utile tient en quelques lignes.

II-A. Les objets JSON

La requête http est très simple : http://maps.googleapis.com/maps/api/distancematrix/json?parameters

Les paramètres sont :

  • origins : les points de départ séparés par des " pipe " ;
  • destinations : les points d'arrivée séparés par des " pipe " ;
  • sensor : à false ;
  • userip : l'adresse IP du client qui fait la requête. Cela permet d'indiquer à Google que ce n'est pas toujours le même client qui initie les requêtes.

Voici l'URL pour le village de BouayeURL pour le village de Bouaye :

http://maps.googleapis.com/maps/api/distancematrix/json?origins=Bouaye,%20Loire-Atlantique&destinations=C.H.U.%20Saint-Jacques,%2044200%20Quartiers%20Sud,%20Nantes|Saint-Mars-de-Coutais|47.123819,-1.66783|Saint-Herblain&sensor=false&userip=192.168.1.1

Une fois la requête émise, voici ce qui revient :

Code JSON de la réponse
Sélectionnez
{
   "destination_addresses" : [
      "C.H.U. Saint-Jacques, 44200 Nantes, France",
      "Saint-Mars-de-Coutais, France",
      "Route du Lac, 44860 Saint-Aignan-Grandlieu, France",
      "Saint-Herblain, France"
   ],
   "origin_addresses" : [ "Bouaye, France" ],
   "rows" : [
      {
         "elements" : [
            {
               "distance" : {
                  "text" : "15,2 km",
                  "value" : 15165
               },
               "duration" : {
                  "text" : "23 minutes",
                  "value" : 1381
               },
               "status" : "OK"
            },
            {
               "distance" : {
                  "text" : "5,6 km",
                  "value" : 5593
               },
               "duration" : {
                  "text" : "7 minutes",
                  "value" : 430
               },
               "status" : "OK"
            },
            {
               "distance" : {
                  "text" : "4,0 km",
                  "value" : 3967
               },
               "duration" : {
                  "text" : "6 minutes",
                  "value" : 386
               },
               "status" : "OK"
            },
            {
               "distance" : {
                  "text" : "15,3 km",
                  "value" : 15288
               },
               "duration" : {
                  "text" : "20 minutes",
                  "value" : 1224
               },
               "status" : "OK"
            }
         ]
      }
   ],
   "status" : "OK"
}

La lecture de ce code se fait comme indiqué sur l'image ci-dessous:

Interprétation de code JSON

Avec JSON, on peut se dire qu'une accolade ouverte indique la presence d'un objet. Nommez les en fonction du nom de la propriété.

On identifie les propriétés suivantes :

  • "destination_addresses" : tableau de chaînes ;
  • "origin_addresses" : tableau de chaînes ;
  • "rows" : tableau de "Row" ;
    • "elements" : tableau d'"element" ;
      • "element" :
        • "distance" ;
        • "duration" ;
        • "status".
  • "status" : chaîne.

On constate que les destinations ne sont pas reprises dans chacun des elements. Il faudra donc absolument respecter l'ordre.

Vous pouvez nommer vos classes comme vous le souhaitez. Pour le processus de désérialisation, seul compte le nom des propriétés.

II-B. L'objet GoogleDistanceMatrix

Cet objet contient une série de propriétés de type chaîne ou tableau de chaînes et une de type tableau de Row.

Version JSON:
Sélectionnez
{
   "destination_addresses" : [ "" ],
   "origin_addresses" : [ "" ],
   "rows" : [ {   } ],
   "status" : ""
}
Version C#
Sélectionnez
[Serializable]
[DataContractAttribute]
public class GoogleDistanceMatrix
{
    [DataMemberAttribute]
    public string[] destination_addresses;
    [DataMemberAttribute]
    public string[] origin_addresses;
    [DataMemberAttribute]
    public Row[] rows;
    [DataMemberAttribute]
    public string status;
}

II-C. L'objet Row

Cet objet est encore plus simple, il ne dispose que d'une seule propriété, un tableau d'un nouveau type : Element.

Version JSON
Sélectionnez
{
   "elements" : [ {   } ]
}
Version C#
Sélectionnez
[Serializable]
[DataContractAttribute]
public class Row
{
    [DataMemberAttribute]
    public Element[] elements;
}

II-D. L'objet Element

Cet objet est constitué de trois propriétés dont deux sont des nouveaux objets: Distance et Duration.

Version JSON
Sélectionnez
{
   "distance" : {   },
   "duration" : {   },
   "status" : "OK"
}
Version C#
Sélectionnez
[Serializable]
[DataContractAttribute]
public class Element
{
    [DataMemberAttribute]
    public Distance distance;
    [DataMemberAttribute]
    public Duration duration;
    [DataMemberAttribute]
    public string status;
}

II-E. L'objet Distance

Très simple, il se compose de deux propriétés, une chaîne et un entier. L'entier est la valeur en mètres de la distance.

Version JSON
Sélectionnez
{
   "text" : "15,2 km",
   "value" : 15165
}
Version C#
Sélectionnez
[Serializable]
[DataContractAttribute]
public class Distance
{
    [DataMemberAttribute]
    public int value;
    [DataMemberAttribute]
    public string text;
}

II-F. L'objet Duration

Encore très simple, il se compose de deux propriétés, une chaîne et un entier. L'entier est la valeur en secondes de la durée.

Version JSON
Sélectionnez
{
   "text" : "23 minutes",
   "value" : 1381
}
Version C#
Sélectionnez
[Serializable]
[DataContractAttribute]
public class Duration
{
    [DataMemberAttribute]
    public int value;
    [DataMemberAttribute]
    public string text;
}

II-G. Le modèle complet

Modèle de données du Distance Matrix

III. La logique métier

Pour pouvoir utiliser la désérialisation JSON, vous devez référencer l'espace de nom "System.Runtime.Serialization", et utiliser "System.Runtime.Serialization.Json" dans votre programme.

III-A. La méthode de désérialisation

Comme je le disais précédement, grâce au Framework 4, elle tient en quelques lignes:

Méthode de désérialisation de la réponse JSON
Sélectionnez
public static GoogleDistanceMatrix GetMatrix(string origins, string destinations, IPAddress ip)
{
   Uri uri = new Uri(string.Format("http://maps.googleapis.com/maps/api/distancematrix/json?origins={0}&destinations={1}&sensor=false&userip={2}",
      origins, destinations, ip.ToString()));

   string rep = GetRequest(uri);
   GoogleDistanceMatrix gdm = new GoogleDistanceMatrix();

   using (MemoryStream mem = new MemoryStream(Encoding.UTF8.GetBytes(rep)))
   {
      DataContractJsonSerializer ser = new DataContractJsonSerializer(gdm.GetType());
      gdm = ser.ReadObject(mem) as GoogleDistanceMatrix;
   }

   return gdm;
}

/// <summary>
/// Méthode pour envoyer la requête http
/// </summary>
/// <param name="uri"></param>
/// <returns></returns>
private static string GetRequest(Uri uri)
{
    string answer = string.Empty;

   HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
   using (HttpWebResponse res = (HttpWebResponse)req.GetResponse())
   {
      if (req.HaveResponse && res.StatusCode == HttpStatusCode.OK)
         using (Stream resin = res.GetResponseStream())
         {
            using (StreamReader rea = new StreamReader(resin))
            {
               answer = rea.ReadToEnd();
            }
         }
   }

    return answer;
}

Copiez ce code dans une classe à part. Ainsi, vous pourrez l'utiliser dans d'autres applications.

III-B. Exploitation de l'objet GoogleDistanceMatrix

Créez une application console. Vérifiez que la propriété "Target Framework" de votre projet est bien à ".Net Framework 4" et non ".Net Framework 4 Client Profile". Ce dernier n'est pas compatible avec la référence "System.Web". Vous ne pourrez pas compiler.

Ajoutez les objets du modèle ainsi que les méthodes de désérialisation et de requête http. Pour monter une architecture selon les règles de l'art, vous pouvez toujours aller faire un petit tour sur ma page d'accueil, vous y trouverez un article sur l'architecture multicouche.

III-B-1. Le fichier de configuration

Afin d'éviter de recompiler l'application à chaque fois qu'on souhaite ajouter des localisations, nous allons utiliser le fichier app.config. Le noeud "configSections" doit être le tout premier.

Paramètres de configuration des points de départ et de destination
Sélectionnez
<configSections>
   <sectionGroup name="Cities">
      <section name="origins" type="System.Configuration.NameValueSectionHandler"/>
      <section name="destinations" type="System.Configuration.NameValueSectionHandler"/>
   </sectionGroup>
</configSections>
<Cities>
   <origins>
      <add key="Bouaye" value="Bouaye, Loire-Atlantique, France" />
      <add key="SaintMars" value="Saint-Mars-de-Coutais, Loire-Atlantique, France" />
      <add key="SaintAignanGrandlieu" value="Saint-Aignan-Grandlieu, Loire-Atlantique, France" />
      <add key="LesSonnières" value="Les Sorinières, Loire-Atlantique, France" />
   </origins>
   <destinations>
      <add key="CHU" value="C.H.U. Saint-Jacques, 44200 Nantes, France" />
      <add key="SaintMars" value="Saint-Mars-de-Coutais, Loire-Atlantique, France" />
      <add key="LeLac" value="Route du Lac, 44860 Saint-Aignan-Grandlieu, France" />
      <add key="SaintHerblain" value="Saint-Herblain, France" />
   </destinations>
</Cities>

Si vous voulez plus de détails sur cette methode, lisez le tutoriel de Nico-pyright(c) Travailler avec les fichiers de configuration en C#.

III-B-2. Le programme

J'espère que les commentaires du code parleront d'eux-mêmes.

Version C#
Sélectionnez
namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            // Récupération de la configuration
            NameValueCollection origins = (NameValueCollection)ConfigurationManager.GetSection("Cities/origins");
            NameValueCollection destinations = (NameValueCollection)ConfigurationManager.GetSection("Cities/destinations");

            // Concatenation des destinations pour former le paramètre "destinations" de l'URL
            string destConcat = ConcatDestinations(destinations);

            // Nous allons stocker les objets désérialisés dans une liste
            List<GoogleDistanceMatrix> list = new List<GoogleDistanceMatrix>();

            // Création d'un objet pour chaque point de départ.
            // L'API supporterait très bien une seule requête avec tous les points de départ. Il faudrait ensuite décomposer les collections.
            // Je préfère faire une requête pour chaque point de départ et les envoyer les unes après les autres.
            foreach (var origin in origins)
            {
                list.Add(GoogleMapManager.GetMatrix(origins[origin.ToString()], destConcat, IPAddress.Parse("192.168.1.10")));
            }

            // Affichage du résultat à l'écran de la console
            foreach (var gdm in list)
            {
                Console.WriteLine(string.Format("Distance/Durée depuis: {0}{1}", gdm.origin_addresses[0].Split(',').FirstOrDefault(), Environment.NewLine));

                for (int i = 0; i < gdm.destination_addresses.Length; i++)
                {
                    Console.Write("\t-> {0}{1}",
                        gdm.destination_addresses[i].Split(',').FirstOrDefault(),
                        ReturnSpaces(gdm.destination_addresses[i].Split(',').FirstOrDefault(), 25));
                    Console.WriteLine("\t{0}\t\t{1}", gdm.rows[0].elements[i].distance.text, gdm.rows[0].elements[i].duration.text);
                }
                Console.WriteLine();
            }

            // Convertion du résultat dans un StringBuilder en vue d'une sauvegarde au format csv.
            StringBuilder csv = new StringBuilder();
            GenerateCsvHeader(csv, destinations);
            foreach (var gdm in list)
            {
                AddDistances(gdm, csv);
                AddTime(gdm, csv);
            }

            // Ecriture du fichier dans le repertoire d'execution du programme.
            using (StreamWriter sw = new StreamWriter("Distances.csv", false, Encoding.UTF8))
            {
                sw.Write(csv.ToString());
            }

            Console.WriteLine("C'est fini. Appuyez sur une touche.");
            Console.ReadLine();
        }

        /// <summary>
        /// Permet de concaténer les localisation en les séparant avec des "pipe".
        /// </summary>
        /// <param name="destinations"></param>
        /// <returns></returns>
        private static string ConcatDestinations(NameValueCollection destinations)
        {
            string sDestinations = string.Empty;

            foreach (var item in destinations)
            {
                sDestinations += string.Format("{0}|", destinations[item.ToString()].ToString());
            }
            return sDestinations;
        }

        /// <summary>
        /// Génère les en-têtes de colonnes
        /// </summary>
        /// <param name="csv"></param>
        /// <param name="destinations"></param>
        private static void GenerateCsvHeader(StringBuilder csv, NameValueCollection destinations)
        {
            csv.Append("\"Points de départ\";\"Type\";");
            foreach (var item in destinations)
            {
                csv.AppendFormat("\"{0}\";", destinations[item.ToString()].Split(',').FirstOrDefault());
            }
            csv.Append(Environment.NewLine);
        }

        /// <summary>
        /// Créé une ligne avec les distances entre le point de départ et les destinations
        /// </summary>
        /// <param name="gdm"></param>
        /// <param name="csv"></param>
        private static void AddDistances(GoogleDistanceMatrix gdm, StringBuilder csv)
        {
            csv.AppendFormat("\"{0}\";\"Distance\";", gdm.origin_addresses[0].Split(',').FirstOrDefault());

            for (int i = 0; i < gdm.destination_addresses.Length; i++)
            {
                csv.AppendFormat("{0};", gdm.rows[0].elements[i].distance.value / 1000);
            }

            csv.Append(Environment.NewLine);
        }

        /// <summary>
        /// Créé une ligne avec les durées entre le point de départ et les destinations
        /// </summary>
        /// <param name="gdm"></param>
        /// <param name="csv"></param>
        private static void AddTime(GoogleDistanceMatrix gdm, StringBuilder csv)
        {
            csv.AppendFormat("\"{0}\";\"Durée\";", gdm.origin_addresses[0].Split(',').FirstOrDefault());

            for (int i = 0; i < gdm.destination_addresses.Length; i++)
            {
                csv.AppendFormat("{0};", gdm.rows[0].elements[i].duration.value / 60);
            }

            csv.Append(Environment.NewLine);
        }

        /// <summary>
        /// Ajoute des espaces au bout de la valeur du point de départ. C'est plus joli sur l'écran de la console :)
        /// </summary>
        /// <param name="sentence"></param>
        /// <param name="nb"></param>
        /// <returns></returns>
        private static string ReturnSpaces(string sentence, int nb)
        {
            string sp = string.Empty;

            for (int i = 0; i < nb - sentence.Length; i++)
            {
                sp += " ";
            }
            return sp;
        }
    }
}

III-B-3. Résultat

Executez le programme. La console devrait afficher ceci:

Affichage de la console


Une fois le fichier CSV ouvert avec Excel, vous pouvez mettre en forme les données ainsi:

Mise en forme des données CSV

IV. Conclusion

Ainsi, certaines des petites difficultés de la vie quotidiennes peuvent souvent être résolues avec quelques lignes de code. Avec ces informations en mains, nul doute que Monsieur Leguilvinec fera un choix éclairé.

Si jamais Visual Studio pouvait référencer les service Web JSON directement ce serait encore plus simple.

Remerciements

Merci à _Max__Max_ pour sa relecture.