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 !
Comments
Anonymous
December 24, 2009
Bonjour, merci pour cet excellent tuto bien que je ne parvienne pas à mes fin pour "Récupérer sa position Avec le GPS" Ne faut'il déjà pas ouvrir le gps ? avec "myGps.Open();" Mon problème est tant que je ne lance pas un autre logiciel de navigation (qui semble lancer le GPS intégré) il ne me géocalise pas...Anonymous
November 18, 2012
Bonjour, Auriez vous une idée de comment on peut faire ça avec Windows Phone 7/8 ?