La Boite à Outils de l’Application Géolocalisée: Partie 1: GPS, et CellId

Cet article fait partie d'une série de 3: La Boite à Outils de l'Application Géolocalisée

Une des révolutions de l'intégration du GPS dans le smartphone, c'est la possibilité pour l'utilisateur de se localiser ou qu'il soit et de trouver des informations en rapport avec sa position: que ce soit une carte ou des directions pour aller quelque part, les horaires de métro de la station d'à coté, les critiques du restaurant devant lequel il passe, etc.

Windows Mobile propose un certain nombre d’outils pour facilement trouver la localisation de l’utilisateur: avec ou sans GPS. Cet article va détailler un certain nombre d’outils pratiques pour l’application qui voudrait proposer du contenu géolocalisé.

Récupérer sa position

Avec le GPS

Le SDK Windows Mobile propose un code d’exemple (Sample Code) qui expose en .NET les API natives permettant d’accéder au GPS. La première chose à faire est donc de compiler ce sample qui est situé dans C:\Program Files\Windows Mobile 6 SDK\Samples\PocketPC\CS\GPS. Ceci étant fait, on peut référencer la DLL générée (qui se trouve dans bin/Debug ou bin/Release) dans un projet et ajouter :

using Microsoft.WindowsMobile.Samples.Location;

Ceci étant fait, l’utilisation du GPS est pire que simple. Voici un tout petit bout de code qui permet de récupérer la position courante et qui le place dans le champ Text d’un contrôle de type Label:

Gps myGps = new Gps();

GpsPosition myPosition = myGps.GetPosition();

if (myPosition.LatitudeValid && myPosition.LongitudeValid)

{

    lblLatResult.Text = myPosition.Latitude.ToString();

    lblLngResult.Text = myPosition.Longitude.ToString();

}

else

{

    MessageBox.Show("Can't get a valid position");

}

myGps.Close();

Avec l’identifiant de cellule (CellId)

Si on veut faire de la géolocalisastion sans avoir de GPS (soit parce qu’il n’y en a pas sur le smartphone, soit parce que la couverture GPS ne le permet pas (à l’intérieur d’un bâtiment par exemple) il est possible de récupérer une localisation à quelques dizaines, voire centaines de mètres près, en utilisant l’identifiant de la cellule dans laquelle le téléphone est connecté au réseau de l’opérateur. En effet, il existe des bases de données qui permettent de faire correspondre un identifiant de cellule à un couple latitude/longitude.

L’identifiant de station de base est en fait constitué de 4 champs différents :

- L’identifiant de pays : (Mobile Country Code)

- L’identifiant de réseau : (Mobile Network Code)

- Le code de zone (Location Area Code)

- L’identifiant de la cellule : (Cell Identifier)

Ces quatre champs peuvent être récupérés avec une fonction de l’API RIL (Radio Interface Layer) : RIL_GetCellTowerInfo.

Pour utiliser cette API depuis un programme en .NET il faut donc utiliser un wrapper, mais je n’ai pas besoin de le réinventer : un développeur de la communauté .NET, Dale Lane, l’a déjà fait !

- Son code : https://dalelane.co.uk/blog/post-images/080312-rilcode.cs.txt

- Son Blog : https://dalelane.co.uk/blog/

J’ai légèrement modifié le code de Dale, qui à l’origine ne retournait pas de Mobile Network Code. Maintenant, la fonction GetCellTowerInfo() retourne les 4 champs séparés par des tirets de la façon suivante : CellId-LAC-MCC-MNC :

class CellIdWrapper

{

    // string used to store the CellID string

    private static string celltowerinfo = "";

    /*

     * Uses RIL to get CellID from the phone.

     */

    public static string GetCellTowerInfo()

    {

        // initialise handles

        IntPtr hRil = IntPtr.Zero;

        IntPtr hRes = IntPtr.Zero;

        // initialise result

        celltowerinfo = "";

        // initialise RIL

        hRes = RIL_Initialize(1, // RIL port 1

                              new RILRESULTCALLBACK(rilResultCallback), // function to call with result

           null, // function to call with notify

                              0, // classes of notification to enable

                              0, // RIL parameters

                              out hRil); // RIL handle returned

        if (hRes != IntPtr.Zero)

        {

            return "Failed to initialize RIL";

        }

        // initialised successfully

        // use RIL to get cell tower info with the RIL handle just created

        hRes = RIL_GetCellTowerInfo(hRil);

        // wait for cell tower info to be returned

        waithandle.WaitOne();

        // finished - release the RIL handle

        RIL_Deinitialize(hRil);

        // return the result from GetCellTowerInfo

        return celltowerinfo;

    }

    // event used to notify user function that a response has

    // been received from RIL

    private static AutoResetEvent waithandle = new AutoResetEvent(false);

    public static void rilResultCallback(uint dwCode,

                                         IntPtr hrCmdID,

                                         IntPtr lpData,

                                         uint cbData,

                                         uint dwParam)

    {

        // create empty structure to store cell tower info in

        RILCELLTOWERINFO rilCellTowerInfo = new RILCELLTOWERINFO();

        // copy result returned from RIL into structure

        Marshal.PtrToStructure(lpData, rilCellTowerInfo);

        // get the bits out of the RIL cell tower response that we want

        celltowerinfo = rilCellTowerInfo.dwCellID + "-" +

                        rilCellTowerInfo.dwLocationAreaCode + "-" +

                        rilCellTowerInfo.dwMobileCountryCode + "-" +

                        rilCellTowerInfo.dwMobileNetworkCode;

        // notify caller function that we have a result

        waithandle.Set();

    }

    // -------------------------------------------------------------------

    // RIL function definitions

    // -------------------------------------------------------------------

    /*

     * Function definition converted from the definition

     * RILRESULTCALLBACK from MSDN:

     *

     * https://msdn2.microsoft.com/en-us/library/aa920069.aspx

     */

    public delegate void RILRESULTCALLBACK(uint dwCode,

                                           IntPtr hrCmdID,

                                    IntPtr lpData,

                                           uint cbData,

                                           uint dwParam);

    /*

     * Function definition converted from the definition

     * RILNOTIFYCALLBACK from MSDN:

     *

     * https://msdn2.microsoft.com/en-us/library/aa922465.aspx

     */

    public delegate void RILNOTIFYCALLBACK(uint dwCode,

                                           IntPtr lpData,

                                           uint cbData,

                uint dwParam);

    /*

     * Class definition converted from the struct definition

     * RILCELLTOWERINFO from MSDN:

     *

     * https://msdn2.microsoft.com/en-us/library/aa921533.aspx

     */

    public class RILCELLTOWERINFO

    {

        public uint cbSize;

        public uint dwParams;

        public uint dwMobileCountryCode;

        public uint dwMobileNetworkCode;

        public uint dwLocationAreaCode;

        public uint dwCellID;

        public uint dwBaseStationID;

        public uint dwBroadcastControlChannel;

        public uint dwRxLevel;

        public uint dwRxLevelFull;

        public uint dwRxLevelSub;

        public uint dwRxQuality;

        public uint dwRxQualityFull;

        public uint dwRxQualitySub;

        public uint dwIdleTimeSlot;

        public uint dwTimingAdvance;

        public uint dwGPRSCellID;

        public uint dwGPRSBaseStationID;

        public uint dwNumBCCH;

    }

    // -------------------------------------------------------------------

    // RIL DLL functions

    // -------------------------------------------------------------------

    /* Definition from: https://msdn2.microsoft.com/en-us/library/aa919106.aspx */

    [DllImport("ril.dll")]

    private static extern IntPtr RIL_Initialize(uint dwIndex,

                                                RILRESULTCALLBACK pfnResult,

                                                RILNOTIFYCALLBACK pfnNotify,

                                                uint dwNotificationClasses,

                                                uint dwParam,

                                                out IntPtr lphRil);

    /* Definition from: https://msdn2.microsoft.com/en-us/library/aa923065.aspx */

    [DllImport("ril.dll")]

    private static extern IntPtr RIL_GetCellTowerInfo(IntPtr hRil);

    /* Definition from: https://msdn2.microsoft.com/en-us/library/aa919624.aspx */

    [DllImport("ril.dll")]

    private static extern IntPtr RIL_Deinitialize(IntPtr hRil);

}

J’intègre le code de Dale dans une classe CellIdWrapper que je rajoute à mon projet dans un fichier séparé.

Puis, dans le code de mon application, je récupère ces informations de la façon suivante :

string cellIdInfo = CellIdWrapper.GetCellTowerInfo();

string[] splittedInfos = cellIdInfo.Split('-');

lblCellIdResult.Text = splittedInfos[0];

lblLACResult.Text = splittedInfos[1];

lblMCCResult.Text = splittedInfos[2];

lblMNCResult.Text = splittedInfos[3];

Il faut maintenant que je fasse correspondre cet identifiant à une latitude et une longitude. Pour cela, j’ai trouvé 2 bases différentes à utiliser :

- Une API « cachée » de Google Maps

- Une API ouverte du groupe OpenCellID

L’API Cachée de Google Maps

Petit disclaimer obligatoire : il ne s’agit pas d’un API officielle, elle n’est donc pas supportée par Google, il est tout à fait possible qu’un jour ça ne marche plus…

Encore une fois, pas besoin de tout réécrire depuis zéro, la communauté va travailler pour moi. Un développeur du nom de Neil Young a développé un bout de wrapper qui fait exactement ce que je veux. Problème, son site n’existe plus, heureusement il a été repris :

- Article de Wei Meng sur DevX : https://www.devx.com/wireless/Article/39709/1954

Comme avec le wrapper de Dale Lane, je rajoute ce wrapper dans un fichier séparé, dans une classe que j’appelle GoogleMapsApi.

static byte[] PostData(int MCC, int MNC, int LAC, int CID,

                       bool shortCID)

{

    /* The shortCID parameter follows heuristic experiences:

     * Sometimes UMTS CIDs are build up from the original GSM CID (lower 4 hex digits)

     * and the RNC-ID left shifted into the upper 4 digits.

     */

    byte[] pd = new byte[] {

        0x00, 0x0e,

        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

        0x00, 0x00,

        0x00, 0x00,

        0x00, 0x00,

        0x1b,

        0x00, 0x00, 0x00, 0x00, // Offset 0x11

        0x00, 0x00, 0x00, 0x00, // Offset 0x15

        0x00, 0x00, 0x00, 0x00, // Offset 0x19

        0x00, 0x00,

        0x00, 0x00, 0x00, 0x00, // Offset 0x1f

        0x00, 0x00, 0x00, 0x00, // Offset 0x23

        0x00, 0x00, 0x00, 0x00, // Offset 0x27

        0x00, 0x00, 0x00, 0x00, // Offset 0x2b

        0xff, 0xff, 0xff, 0xff,

        0x00, 0x00, 0x00, 0x00

    };

    bool isUMTSCell = ((Int64)CID > 65535);

    if (isUMTSCell)

        Console.WriteLine("UMTS CID. {0}", shortCID ?

            "Using short CID to resolve." : "");

    else

        Console.WriteLine("GSM CID given.");

    if (shortCID)

        CID &= 0xFFFF; /* Attempt to resolve the cell using the

                            GSM CID part */

    if ((Int64)CID > 65536) /* GSM: 4 hex digits, UTMS: 6 hex

                            digits */

        pd[0x1c] = 5;

    else

        pd[0x1c] = 3;

    pd[0x11] = (byte)((MNC >> 24) & 0xFF);

    pd[0x12] = (byte)((MNC >> 16) & 0xFF);

    pd[0x13] = (byte)((MNC >> 8) & 0xFF);

  pd[0x14] = (byte)((MNC >> 0) & 0xFF);

    pd[0x15] = (byte)((MCC >> 24) & 0xFF);

    pd[0x16] = (byte)((MCC >> 16) & 0xFF);

    pd[0x17] = (byte)((MCC >> 8) & 0xFF);

    pd[0x18] = (byte)((MCC >> 0) & 0xFF);

    pd[0x27] = (byte)((MNC >> 24) & 0xFF);

    pd[0x28] = (byte)((MNC >> 16) & 0xFF);

    pd[0x29] = (byte)((MNC >> 8) & 0xFF);

    pd[0x2a] = (byte)((MNC >> 0) & 0xFF);

    pd[0x2b] = (byte)((MCC >> 24) & 0xFF);

    pd[0x2c] = (byte)((MCC >> 16) & 0xFF);

    pd[0x2d] = (byte)((MCC >> 8) & 0xFF);

    pd[0x2e] = (byte)((MCC >> 0) & 0xFF);

    pd[0x1f] = (byte)((CID >> 24) & 0xFF);

    pd[0x20] = (byte)((CID >> 16) & 0xFF);

    pd[0x21] = (byte)((CID >> 8) & 0xFF);

    pd[0x22] = (byte)((CID >> 0) & 0xFF);

    pd[0x23] = (byte)((LAC >> 24) & 0xFF);

    pd[0x24] = (byte)((LAC >> 16) & 0xFF);

    pd[0x25] = (byte)((LAC >> 8) & 0xFF);

    pd[0x26] = (byte)((LAC >> 0) & 0xFF);

    return pd;

}

static public string GetLatLng(string[] args)

{

    if (args.Length < 4)

    {

        return string.Empty;

    }

    string shortCID = ""; /* Default, no change at all */

    if (args.Length == 5)

        shortCID = args[4].ToLower();

    try

    {

        String url = "https://www.google.com/glm/mmap";

        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(

            new Uri(url));

        req.Method = "POST";

        int MCC = Convert.ToInt32(args[0]);

        int MNC = Convert.ToInt32(args[1]);

        int LAC = Convert.ToInt32(args[2]);

        int CID = Convert.ToInt32(args[3]);

        byte[] pd = PostData(MCC, MNC, LAC, CID,

            shortCID == "shortcid");

        req.ContentLength = pd.Length;

        req.ContentType = "application/binary";

        Stream outputStream = req.GetRequestStream();

        outputStream.Write(pd, 0, pd.Length);

        outputStream.Close();

        HttpWebResponse res = (HttpWebResponse)req.GetResponse();

        byte[] ps = new byte[res.ContentLength];

        int totalBytesRead = 0;

        while (totalBytesRead < ps.Length)

        {

            totalBytesRead += res.GetResponseStream().Read(

                ps, totalBytesRead, ps.Length - totalBytesRead);

        }

        if (res.StatusCode == HttpStatusCode.OK)

        {

            short opcode1 = (short)(ps[0] << 8 | ps[1]);

    byte opcode2 = ps[2];

            int ret_code = (int)((ps[3] << 24) | (ps[4] << 16) |

                           (ps[5] << 8) | (ps[6]));

            if (ret_code == 0)

            {

                double lat = ((double)((ps[7] << 24) | (ps[8] << 16)

                             | (ps[9] << 8) | (ps[10]))) / 1000000;

                double lon = ((double)((ps[11] << 24) | (ps[12] <<

                             16) | (ps[13] << 8) | (ps[14]))) /

                             1000000;

              return lat + "|" + lon;

            }

            else

                return string.Empty;

        }

        else

            return string.Empty;

    }

    catch (Exception ex)

    {

        return ex.Message;

    }

}

Super simple à réutiliser depuis mon code avec la méthode GetLatLng à qui on passe un tableau de string avec dans l’ordre MCC, MNC, LAC et CellId, et qui me renvoie dans une chaine de caractères la latitude et la longitude séparée par un caractère ‘|’ (pipe) :

string result = GoogleMapsApi.GetLatLng(args);

string[] splittedResult = result.Split('|');

lblLatResult.Text = splittedResult[0];

lblLngResult.Text = splittedResult[1];

L’API ouverte du groupe OpenCellID

OpenCellId est une initiative qui vise à crowdsourcer un maximum de coordonnées de tour cellulaires. C’est un peu moins précis que la base de données de Google, mais c’est en participant que ca s’améliorera J

- Le site web : https://opencellid.org/

- L’application Windows Mobile pour aider à crowdsourcer les informations : https://opencellclient.sourceforge.net/

Pour utiliser l’API OpenCellID, il faut demander une clé d’API, qui est envoyée automatiquement lorsqu’on s’enregistre sur le site d’OpenCellID : https://opencellid.org/users/signup.

L’API OpenCellID est très simple : c’est du REST, donc j’envoie une requête http, et ils me renvoient les informations dans du XML : l’API est détaillée à l’adresse suivante : https://opencellid.org/api

Pour plus d’homogénéité dans mon projet je vais encore créer un autre fichier avec à l’intérieur une classe OpenCellIdApi qui contiendra, comme le wrapper pour l’API Google, une fonction GetLatLng, avec le même tableau d’arguments en entrée, et la même chaine de caractères en sortie.

Voici à quoi ressemble du code pour interroger la base OpenCellID :

struct OCIWebServiceResponse

{

    public string lat;

    public string lng;

    public string status;

    public string nbSamples;

    public string range;

}

static public string GetLatLng(string[] args)

{

    if (args.Length < 4)

    {

        return "Not Enough Arguments";

    }

    OCIWebServiceResponse response;

    StringBuilder urlArgs = new StringBuilder("key=" + myApiKey);

    urlArgs.Append("&mnc=" + args[1]);

    urlArgs.Append("&mcc=" + args[0]);

    urlArgs.Append("&lac=" + args[2]);

    urlArgs.Append("&cellid=" + args[3]);

    try

    {

      WebRequest myRequest = WebRequest.Create(new Uri(BaseUrl + urlArgs.ToString()));

        WebResponse myResponse = myRequest.GetResponse();

        XmlReader myReader = XmlReader.Create(myResponse.GetResponseStream());

        try

        {

            response = (from rsp in XDocument.Load(myReader).Descendants("rsp")

                        let cell = rsp.Descendants("cell").Single()

                        select new OCIWebServiceResponse

                        {

                            status = rsp.Attribute("stat").Value,

                            lat = cell.Attribute("lat").Value,

                            lng = cell.Attribute("lon").Value,

                            nbSamples = cell.Attribute("nbSamples").Value,

                            range = cell.Attribute("range").Value

                        }).Single();

        }

        finally

        {

            myReader.Close();

            myResponse.Close();

        }

    }

    catch (Exception ex)

    {

        return ex.Message;

    }

    if (response.status != "ok")

        return "Response from OpenCellId not OK!";

    else

        return response.lat + "|" + response.lng;

}

Pour parser la réponse en XML, j’utilise une requête LINQ et une structure « maison » qui rend mon code plus lisible.

Point important, c’est du code de démo, la gestion des erreurs est assez pauvre, même carrément mauvaise : il ne faudrait pas retourner une chaine de caractère comme je le fais mais plutôt lever une exception !

Prochaine étape: Geocoding d'adresse !