Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Using the Skyhook Wireless XPS Positioning Service in Managed Code

0.00/5 (No votes)
20 Jan 2009 1  
Wrapper and sample programs demonstrating the use of the Skyhook Wireless XPS SDK (hybrid position system using GPS, WiFi Positioning, and Celltower positioning)

screenshot

Introduction

Skyhook wireless is well known for providing WiFi based positioning. The service is used on the iPhone, the Eye-Fi geotagging memory cards for cameras, and is available for the Windows desktop Operating Systems, OS X, Linux, Symbian, and Windows Mobile. Recently Skyhook Wireless updated the SDK to make use of cell tower and GPS location combining information from these three location technologies to give applications an incredibly large area of coverage. For Windows Mobile, the service is accessed through an unmanaged library. From reading through the Skyhook Wireless developer forums, I found that managed developers haven't been able to access 100% of the functionality. I decided to create a wrapper myself..

My first attempt at creating the wrapper failed, and I soon found out why other developers had not succeeded. The interop features available in the Compact Framework were not what was needed to make a wrapper using the usual techniques. Using some time I had over an end-of-year break, I managed to create a wrapper to make available 100% of the service's functionality. Within this article, I revisit a program I posted last year on a program that would automatically respond to an SMS request with a phone's location, adding to it both XPS based positioning and mapping using Microsoft's Virtual Earth web services.

At the time I initially published this article the Skyhook Wireless SDK only performed WiFi based positioning.  They've now updated their client to make use of GPS and Cell Tower location allowing the developer to get back location data from the most precise source available through a single function call.

Technologies and Requirements

The sample programs presented here require a Windows Mobile Professional device running Windows Mobile 5 or Windows Mobile 6. The .NET framework version 3.5 must be present on the device, and the last example program also makes use of SQL Server CE version 3.5. Additionally, you will want a phone that has an SMS plan and an unlimited data plan. The example programs will download map images over your data connection, and images can consume data quickly. The programs presented here are designed to scale up to 800x800 screens.

Terms

There are a few terms I will be using several times throughout this article.

  • WPS - WiFi Positioning Service
  • WPSAPI - The API available from Skyhook Wireless for WiFi Positioning
  • WpsProxy - Native DLL I made to handle translation of some data between the managed code and WPSAPI
  • XPS - A Skyhook Wireless solution that automatically combines GPS and WPS information for reliable and quick position determination.

Setting up your Developer Accounts

Some of the web services I call within the example code require an account to be used. In all cases, a developer account is available at no cost. There is a restriction on the volume of calls that can be made from a developer account. While the number of allowed calls is well more than enough for a single person, if I were to distribute this code with my account information embedded within it, I'm sure the maximum volume of allowed calls would be quickly reached (and I am sure you will understand why I would not want my account information to be shared).

The example programs here will look for the account information in a set of Registry keys. Storing the account information in the Registry allows for the information to be specified once and then be available to all programs that need it. If you attempt to run the programs here without first setting up your own developer accounts, then they will not work. To save your account information to your device, run the code in the project named "SetMyKeys."

You can setup your developer accounts by visiting each of the following URIs:

Why use XPS Positioning

GPS devices can give incredible precise information on one's position. Despite their resolution, GPS devices have what I feel to be two major shortcomings. They can take some time to acquire a fix, and there are many conditions and environments under which a GPS device cannot acquire a fix at all (in "urban canyons" or while inside a building). Even if a clear GPS signal is available, not all Windows Mobile devices have GPS hardware. By making use of other positioning technologies, we can both broaden our potential customer base and improve our program's ability to quickly acquire position information.

How does XPS Positioning Work?

Skyhook Wireless's XPS makes use of three location technologies; WiFi, Cell Tower, and GPS.  WiFi Routers are constantly transmitting their ID number (MAC address). Skyhook Wireless has accumulated a large database of these router IDs and the position where their signals could be received. They hired over 500 full time drivers to drive around picking up WiFi signals to correlate them to GPS positions. Additionally Skyhook Wireless also has a database of cell tower IDs and their location.  When no GPS signal can be received XPS will submit the IDs of nearby routers and cell towers and return your position. Skyhook Wireless explains the technology on this page.

The WPSAPI library also makes use of intelligent caching. The developer can specify a cache size, and the WPSAPI will download a subset of the database to the local device's storage. After the download, the WPSAPI does not need to make additional round trips to the Skyhook Wireless server to get position information until the user moves outside of the area that is covered by the cached information. In addition to getting position, the service can also geocode one's position, providing the state, city, and even street on which the user is standing.

What is the Coverage Like in my Area?

Finding a place where the XPS client is unable to find a location is pretty difficult.  Its coverage through GPS, cell tower, and WiFi positioning is excellent. If you would like to see what type of WiFi coverage Skyhook Wireless has in your area, you can check the coverage map. At first glance of the map for the United States, the coverage looks a little sparse. I then realized that a lot of the population is located in or around metro areas, and compared the coverage map to population density maps. Looking at things this way, I saw that coverage was pretty good. I live within a suburban area outside of Atlanta, GA, USA. I drove along a 7 mile path from my house to a restaurant, with my phone polling my position through WPS, and only encountered a single section of road in which my phone could not see a WiFi signal to infer my position.  Even when outside of an area that has WiFi location coverage remember that the XPS client will also use cell tower and GPS location.

Skyhook Wireless WiFi coverage map and population density map for Phoenix, Arizona, USA. The fifth largest city in the United States. Darker blue indicates denser population. Notice that the shape of the population density and WiFi coverage is similar.

Acquiring the Skyhook Wireless SDK

The Skyhook Wireless SDK is a free download that only requires you to register for a developer account. When you open your account, you specify your identity with a User ID and a Realm. There is no password. A realm is just some name you decide to use to identify your company or group. It could be an entity name, a domain name, or any of a number of names of your choosing. Once you've registered and have logged in, you'll see a number of different SDKs and files that you can download. There are two files that you will want to download for this article. One is the Windows Mobile 5 & 6 PocketPC (arm) for cell phones and Virtual GPS for Windows Mobile PocketPC (arm) for cell phones. It is available for a number of other platforms, but for the sake of this article, I will only consider the Windows Mobile downloads.

Regardless of whether or not you are working with the project attached to this article, if you are using the Skyhook Wireless WPSAPI SDK, you will need to update the properties of the WpsProxy class so that it knows where the header file and the library file are for the SDK. To change these settings within Visual Studio, right-click on the C/C++ project that is using the WPSAPI library, and select Properties. The setting for the additional include directories is under Configuration Properties-> C/C++->General. The setting for Additional Libraries can be found under Configuration Properties-> Linker ->Additional Library Directories.

Making Immediate Use of WPS Without Changing Your GPS Based Programs

If you already have applications that use GPS, you can make immediate use of the Skyhook Wireless WPS service through the Virtual GPS application. This is especially useful for devices that have no GPS hardware. The Virtual GPS application emulates a GPS device. Software that communicates with a GPS device either through a COM port or the GPS intermediate driver can make use of Virtual GPS. If your device has GPS hardware, Virtual GPS can be configured to use both the GPS hardware and WPS to acquire your position. Your software will receive GPS information when available, and will fallback on WPS information when GPS information is not available. I won't detail Virtual GPS setup here, since Skyhook Wireless already has made available document detailing how to setup Virtual GPS.

Challenges in P/Invoking the WPS Code

When I made the first attempt at creating a wrapper for the WPSAPI, the pathway I took is the same that I've taken for other native code that I've called. With WPSAPI, I found that this wasn't appropriate. The WPSAPI uses ANSI strings (single byte strings), whereas the Compact Framework uses Unicode strings (2 bytes). When marshaling strings on the desktop framework, for this scenario, we could automatically convert from one type of string to another by adding a MarshalAs attribute with UnmanagedType.LPStr to the string field in question. However, this value is not supported in the Compact Framework.

Another complication encountered when making a wrapper is that complex structures are being passed from the WPS API; a structure may contain a pointer to another structure, which may contain a structure to another entity. When trying to pass structures of this nature, the CLR would often return the ever familiar yet non-descriptive NotSupportedException. These two problems together made it difficult for managed developers to access the WSP API.

Rethinking the Interface to the WPSAPI Library

After encountering the obstacle of string conversion and the passing of complex structures, I thought "Wouldn't it be nice if the WPSAPI used Unicode strings and passed flat structures?" Since I don't have the code for the WPSAPI library, I can't change the library's string implementation. So, I made a native DLL of my own to call the WPSAPI functionality. This DLL is in the project WpsProxy. The project is written in C, and primarily exists as a translation layer to sit between managed code and the native API. This layer handles the conversion of Unicode and ANSI strings, and converts the complex WPS structure to a flat structure which can more easily be manipulated from within a .NET language.

The managed wrapper for WpsProxy is in the project J2i.Net.WiFiPositioning in a class named WPS. Almost all the functionality contained within WPS is exposed through the wrapper. The exception is two functions that exist to free resources that were allocated by WPS calls. Instead of passing unmanaged resources to the managed code and making the developer responsible for deallocating it, I allocate the necessary memory from within managed code, and pass it to the proxy for population. After populating the information, the WpsProxy code will deallocate the memory that was allocated by the WPSAPI call.

Below is an example of one of the simplest calls that passes through the proxy:

extern "C" WPSPROXY_API void SetServerUrl(const LPWSTR url)
{
    //Convert unicode string to ANSI string. 
    //this call allocates memory
    LPSTR szUrl = wcstombs(url);
 
    //call WPSAPI function with ANSI string
    WPS_set_server_url(szUrl);
 
    //Free the ANSI string
    delete szUrl;
}

The most difficult functionality to wrap was the functions that involved callbacks. Callbacks from native code can normally be made by passing a managed delegate to the native function and allowing the runtime to handle the marshalling of the call between the two code entities. But, as stated before, the Compact Framework could not handle automatic conversion of the strings and translation of the structures that needed to be passed. To work around this, when a WPSAPI function is called that requires a callback, it receives a pointer to a function in my WpsProxy project. That function will then call back a delegate that it received from managed code. When the managed code returns a value, the WpsProxy component will take that return value and pass it back to the WPSAPI. In cases where the callback function was listed in the WPSAPI documentation as optional, it will still receive a callback function from WpsProxy. If WpsProxy does not have a managed delegate to call, it simply returns control to WPSAPI.

The following is one of the functions in WpsProxy that is called by the managed code. The delegate from the managed code is passed in the parameter callback. The function body performs the conversion from two-byte characters to single byte characters, and then calls a WPSAPI function.

extern "C" WPSPROXY_API WPS_ReturnCode PeriodicLocation(
    LPWSTR userName, 
    LPWSTR realm, 
    WPS_StreetAddressLookup lookupType, 
    unsigned long period, 
    unsigned long iterations, 
    WiFiLocationDelegate* callback
    //This is the delegate passed in from managed code
    )
{
    //perform conversion from double byte strings to single byte strings
    WPS_SimpleAuthentication simpleAuthentication;
    simpleAuthentication.realm = wcstombs(realm);
    simpleAuthentication.username = wcstombs(userName);
    WPS_ReturnCode result;
 
    result = WPS_periodic_location(
        &simpleAuthentication, 
        lookupType,period, 
        iterations, 
        WiFiLocationCallback,        //Function predefined within WPSAPI
        callback                    //managed delegate. Passed as data parameter
        );                    
    //delete userName;
    //delete realm;
    return result;
}

The WpsProxy code required to make the callback work is shown below. I've erased a lot of the code here as the other work that is done by this code is not as important as seeing how the callback mechanism is implemented. WPSAPI performs a callback to this function, and this function will then callback to the managed code (if a delegate has been provided by the managed code), or does not perform the callback if no delegate has been passed.

WPS_Continuation WiFiLocationCallback(void*  arg, WPS_ReturnCode result, 
                 const WPS_Location* location)
{
    WPS_Continuation retCode;
    retCode = WPS_STOP;
    if(arg!=NULL) // if a callback function has been passed
    {
        WiFiLocationDelegate delegateMethod = (WiFiLocationDelegate)arg;
        
        //Code deleted from here for clarity. At the end of the work that is
        //performed by the code that I've deleted here the callback function
        //is called.
        
        retCode = delegateMethod(result,location->latitude, location->longitude, 
                                 location->hpe, location->nap, location->speed, 
                                 location->bearing, streetNumber, addressLine, 
                                 city, stateName, stateCode, postalCode, county, region, 
                                 countryName, countryCode,province);
    }/*arg!=null*/
    return retCode;
}

Using the Wrapper

To use the wrapper, a reference to the J2i.Net.WiFiPositioning.dll assembly must be made. If you add this reference to a project, deploy the project, and then try to call a WPS function, you will receive an error about WPSPROXY.dll not being found. You'll need to manually copy that assembly to the target folder on your device. After doing this, again, if you try to run the project, you will get the exact same error even though the file is there. This error occurs because WpsProxy.dll can't be loaded unless the files to which it occurs are available; you will need to also copy the WPSAPI.DLL file to the target device.

If the above requirements are met, then you can get your location by instantiating a WPS object and then calling a single function to acquire your location. This basic functionality is demonstrated in the project named SimpleClient. Code from this project appears below.

// From the project SimpleClient
        
Wps _wps = new Wps(_userName, _realm);
 
void UpdatePosition(WiFiLocation position)
{
   //...this function list out the values of the fields on the WiFiLocation
   //object passed to it. 
}
private void miFullAddress_Click(object sender, EventArgs e)
{
    WiFiLocation loc = _wps.GetWiFiLocation(StreetAddressLookupType.FullStreet);
    this.UpdatePosition(loc);
}
 
void miPosition_Click()
{
    WiFiLocation loc = _wps.GetWiFiLocation(StreetAddressLookupType.NoStreet);
    this.UpdatePosition(loc);
}

The WPS class has other methods that can be called. I don't detail the purpose of all of the methods here. That information can be found in the WPSAPI documentation on the Skyhook wireless site. The method names I've placed on the WPS class are not the exact same as the function names in the WPSAPI library, but they are similar, and it is not difficult to map each one of the Wps class methods to the WPSAPI functions. In the coming weeks, I will be making more detailed documentation available within the Skyhook Wireless discussion group.

Wps Class Diagram

The two overloaded methods (WPS.GetIPLocation and Wps.GetWiFiLocation) allow for two styles of handling situations where a location cannot be acquired. You can choose to detect these situations either through a return code or by handling an exception.

//These methods will throw an exception if a location cannot be acquired
public WiFiLocation GetWiFiLocation(StreetAddressLookupType lookupType);
public IPLocation GetIPLocation(StreetAddressLookupType lookupType)
 
//These methods will return an error code if a location cannot be acquired
public ResultCode GetWiFiLocation(StreetAddressLookupType lookupType, 
                                  out WiFiLocation location)
public ResultCode GetIPLocation(StreetAddressLookupType lookupType, 
                                out IPLocation location)

Screenshots of a simple client getting basic position and full position information.

The above function calls return instances of WiFiLocation. WiFiLocation and IPLocation are both classes that derive from a Location class. The WPSAPI equivalents of these two classes are independent of each other, but for my wrapper implementation, I've made them derive from a common class.

If you need periodic updates of a user's location, you can have the WPSAPI pool the user's location, and report their location back to you at the frequency of your choosing. I've made this functionality accessible through the method BeginPeriodicLocation. This is a blocking call; the caller will not regain control until the WPS object has no more locations to report. You will need to create your own thread for the periodic updates if you do not want to block the main thread.

ResultCode rc = _wps.BeginPeriodicLocation(
        StreetAddressLookupType.NoStreet, //leve of positioning detail required
        5000,   //period (in milliseconds) desired between updates
        10      //The number of times that a position is to be reported
);

Caching Location Data

Skyhook Wireless made available caching of router IDs and their locations through a facility called tiling. The developer can specify how much memory can be consumed by the cache overall, and how much data can be downloaded from the Skyhook Wireless server in a single session. You will need to specify the file path in which the cached data will be saved. The following will save up to a little under 4 megs of cached data in the "\temp" folder. No more than 500,000 bytes will be downloaded per session.

rc = _wps.BeginTiling("\\temp\\", 500000, 4000000);

Note that tiling only works if you are requesting position only, with no address data. Requesting address data will always result in a call back to the Skyhook Wireless server.

Microsoft's Location Based Web Services

There are a lot of location based Web Services that are available for your use. One of the easiest services to use is the Live Search Web Service. I won't describe Live Search within this article, but if you would like to get started, there is a code example in this CodeProject article. The Live Search location related functionality is best described as being like a phonebook. Best of all, the service is easy to use. The MapPoint and Virtual Earth web services give you a much larger volume of information, but require a little more effort to use. The developer accounts for the MapPoint and Virtual Earth services are the same account, and I've even found, in some cases, I can redirect a program from one service to another, and it will continue to work as designed without any other code needing to be changed.

Service Divisions

Rather than make the functionality of the service available in a single monolithic Web Service, the Virtual Earth and MapPoint Web Services have been divided into functional groups.

  • Common - contains general functionality and calls needed for authentication
  • Geocode - contains functionality for translating between addresses, geographical coordinates, and other methods of naming places
  • Imagery - used to get maps of areas
  • Route - used to get directions
  • Search - used to find businesses within an area.

Within the sample programs here, I make a few calls to the Imagery, Search, Geocode, and Common services. Here are the links to the Virtual Earth WSDLs.

Service URI
Common http://staging.common.virtualearth.net/find-30/common.asmx
Geocode http://staging.dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc?wsdl
Imagery http://staging.dev.virtualearth.net/webservices/v1/imageryservice/imageryservice.svc?wsdl
Route http://staging.dev.virtualearth.net/webservices/v1/routeservice/routeservice.svc?wsdl
Search http://staging.dev.virtualearth.net/webservices/v1/searchservice/searchservice.svc?wsd

Using the Search Service

Open a solution named MapPoint.SimpleClient. This project makes use of data from NavTech through the search to search for businesses that fall within a selected category (the program is preconfigured to work with the North American data source; if you are in Europe, you will need to change the program's settings to use the European data source instead). Select a search category, and then select the "Search" button, and you will see a list of businesses within your area that fall in that category.

private void miSearch_Click(object sender, EventArgs e)
{
    MapPoint.Service.FindServiceSoap findService = 
             new MapPoint.SimpleClient.MapPoint.Service.FindServiceSoap();
 
    //To prevent to annoying URI Exception in the debug output. Optional.
    findService.Proxy = System.Net.GlobalProxySelection.GetEmptyWebProxy(); 
 
    //Specify our credentials for authorization
    findService.Credentials = new NetworkCredential(
                                      _registrySettings.MapPointAppID, 
                                      _registrySettings.MapPointPassword
                                      );
    findService.PreAuthenticate = true;
 
    FindFilter findFilter = new FindFilter();
 
    //http://msdn.microsoft.com/en-us/library/cc534903.aspx
    findFilter.EntityTypeName = _entityDictionary[dupQuery.Text]; 
    
    //only display the display name for each search result
    findFilter.PropertyNames = new string[]{"DisplayName"};
 
    MapPoint.Service.LatLong latLong = new MapPoint.Service.LatLong();
    latLong.Latitude = Double.Parse(txtLatitude.Text);
    latLong.Longitude = Double.Parse(txtLongitude.Text);
 
 
    MapPoint.Service.FindNearbySpecification findSpecification = 
                     new MapPoint.Service.FindNearbySpecification();
    findSpecification.DataSourceName = _registrySettings.MapPointDataSource;
    //If the user has not selected a datasource then we will
    //use the NavTech North American datasource
    if (findSpecification.DataSourceName.Length == 0)
        findSpecification.DataSourceName = "NavTech.NA";
    findSpecification.LatLong = latLong;
    findSpecification.Distance = 100;
    findSpecification.Filter = findFilter;
 
    FindResults resultList = null;
    try
    {
        resultList = findService.FindNearby(findSpecification);
        if (resultList.Results != null)
        {
            DisplayResults(resultList.Results);
        }
    }
    catch(Exception exc)
    {
        System.Diagnostics.Debug.WriteLine(exc.ToString());
    }
}

Displaying a Map

Retrieving a map from Virtual Earth is a multistep process. Using the Common service, a token must be acquired and submitted with each request. Think of the token as being the same thing as a session cookie. It must be submitted for a number of other requests, and expires after a certain amount of time. Once you have the token, you can make a request for a map URI; the call takes the coordinates for the geography that you want in the center of the map, a zoom level, and optionally, the coordinates of pushpins that you would like to display on the map. The result is a string that contains the URI to an image. Your third request in acquiring a map is downloading the image that is at the URL that you received. To walk you through these steps, open the project J2i.Net.MapPoint.RenderMap.

J2i.Net.MapPoint.RenderMap has web references to two services; the common service and the imaging service. Since I am using a development account and not a commercial account, the maps I request will have a "Staging" watermark on them.

RenderMap Example

Revisiting FindMe

In August of 2007, I posted a GPS based Windows Mobile program that would respond with my position either via e-mail or SMS when a special text message was received. One of the shortcomings of the program was that in order to make use of the program's response, you had to have access to a full browser. Even now, a full browser is not a standard component on a Windows Mobile device (though that will be changing over the next few months). At the end of the article, I had expressed interest in adding maps to the program so that its responses could be used without the assistance of a computer.

The first version of this program would only send the phone's location, but would never receive location information from anyone. Within this updated version, the program will both send location and display location information received by other devices. I've also eliminated the use of the PIN that was required to request someone's location. Permissions for location information is controlled completely through attributes added onto one's contact entry.

Using the Emulator to Save Money

While you've got a few Windows Mobile phones in your house that you could use for testing this program, you'll want to test some portions of this program on the emulator. In my opinion SMS is too expensive for doing all of one's testing on real hardware. Here in the United States, both the sender and the receiver of a message are charged. Currently, an SMS message can cost between 0.20 and 0.30 USD (let's just say they cost 0.25 USD). In a single interaction in which one phone sends a message to another and then gets a response, there would be a total of four of these charges to account for. The sending phone incurs a charge when sending a request, the receiving phone incurs another charge for receiving the request, and then the receiving phone responds causing another charge, and the original phone receives the response which is another charge. One interaction alone costs 1.00 USD. I have an SMS message plan on my phone which allows for a limited number of messages before I begin incurring charges. But, it would not take very much time to consume my allotted messages

The emulator helps out a lot here. The SMS related code can be tested using the Windows Mobile Cellular emulator, and you will not incur any charges. The WPSAPI and cell tower location code won't work within the emulator, but one can easily work around this by using FakeGPS (GPS Emulator) or hard coding a location while testing in the emulator before doing final testing on real hardware.

Debugging over WiFi

The WiFi adapter in my phone is usually turned off. In having it turned on while debugging this program, I came across an annoying behaviour. The WiFi adapter in my phone connected to my home network, and when I began debugging, Visual Studio was communicating with my device over WiFi instead of over the USB cable. I verified this by disconnecting the USB cable. After disconnecting it, I was still able to step through the code and start and stop my program. This sounds like a nice ability, but debugging over WiFi is extremely slow. While debugging this program, I removed the settings for my router from the phone to prevent this behaviour.

Code Organization

The code is divided into three distinct parts; interfaces, UI classes, and processing classes. I defined interfaces for several (though not all) classes to assure that some of the classes were not too tightly bound to each other. This makes swapping out implementations a little easier. For example, there is an interface named IGeoManager which declares some methods that are used for geocoding and retrieving maps. VirtualEarthGeoManager implements this interface to retrieve maps from Microsoft's Virtual Earth Web Service. If I decided to use another provider for maps and geocoding, I would only need to define another class that implements this interface, and instantiate it instead of the VirtualEarthGeomanager.

Most of the UI classes, in general, don't have much to speak on beyond what you would expect to find in a form. An exception is the MapDisplay class. It's a custom class for displaying the maps in a finger-friendly manner. The map image given to this control will usually be larger than the size of the control itself, so you can drag the map around within this view. A few UI classes also make use of asynchronous loading. Tasks such as list all of our contacts or downloading a map can take some time, and performing these tasks in a synchronous way results in periods of the program appearing to freeze. On the full framework, a method can be called asynchronously through the BeginInvoke and EndInvoke methods on a delegate. These methods exists on the Compact Framework delegates, but they are not implemented; attempting to call them results in a NotImplemented exception. So, instead of using BeginInvoke, I used the ThreadPool class to run a task on another thread, and used Control.Invoke to marshal the UI changes back to the main thread after the task is complete.

The processing classes are all in a folder called Common. The functionality is divided among several classes, so it should be easy to find the class that holds a specific piece of functionality. Here is a listing of some of the most important classes and their purpose.

Class Description
CellTowerLocationProvider Uses OpenCellID.org for cell tower based location
GpsLocationProvider Uses GPS hardware to determine your location
WiFiLocationProvider Uses Skyhook Wireless's WPSAPI to determine location from nearby routers.
HubLocationProvider Gathers location information from GpsLocationProvider, WiFiLocationProvider, and CellTowerLocationProvider, and returns the coordinates that are most precise and not yet expired
EmailLocationResponder Sends location information using e-mail
LocationResponder Abstract class containing some of the implementation for creating a response message
ContactApproval Used to determine whether or not a contact has permission to see your location
MapRequest Contains the information on a map being requested
SqlLocationLogger Used to log received locations to a local SQL database and retrieve those locations for viewing
Session Envelopes most of the state information for the program
Settings Contains the user's preferences

Choosing a Location

There are three location providers defined in this program; GPS, WPS, and a cell tower based location. The three of these were defined when the latest version of the Skyhook Wireless SDK was 2.7.  Version 2.7 did not contain XPS so I had to collect the location information from these three sources and then aggregate it on my own.  With the release of the 3.0 SDK WPS will automatically aggregate these location sources so the other location providers are not used. I've decided to leave them within the code though, especially for those that would like to use the OpenCellID cell tower location system and need an example of how it can be called.  The location information had been aggregated through the class named  HubLocationPRovider.  The position information returned from each one of the location technologies has a priority. GPS has highest priority since it is the most precise. WPS is second, and cell tower location is last. The location information from each one of these services is also assigned an expiration time; if I've lost a GPS signal momentarily, the last reported GPS location is probably more accurate than the WPS or cell tower location for a short amount of time. The HubLocationProvider won't consider the next available provider until the higher priority provider has expired.

//variable for holding our list of providers
ProviderExpiration[] _providerList;
 
public HubLocationProvider()
{
    //Add the providers to the provider list by order of priority
    _providerList = new ProviderExpiration[3] ;
 
    _providerList[0] = new ProviderExpiration();
    _providerList[0].ExpirationTime = new TimeSpan(0, 0, 30);
    _providerList[0].LocationProvider = _gpsProvider;
 
    _providerList[1] = new ProviderExpiration();
    _providerList[1].ExpirationTime = new TimeSpan(0, 1, 15);
    _providerList[1].LocationProvider = _wifiLocationProvider;
 
    _providerList[2] = new ProviderExpiration();
    _providerList[2].ExpirationTime = new TimeSpan(0, 2, 0);
    _providerList[2].LocationProvider = _cellProvider;
}
 
public Location LastLocation
{
    get 
    {
        // Go through all of the available location providers by order of
        //priority and return the highest priority non-expired value
        for (int i = 0; i < _providerList.Length; ++i)
        {
            if (!_providerList[i].IsExpired())
                return _providerList[i].LocationProvider.LastLocation;
        }
        return null;
    }
}

Detecting a Location Request

Location requests are sent via SMS. Using the MessageInterceptor class, any message that arrives that meets a specific set of requirements will be routed to the FindMe program. The properties of the messages in which we have interest are packaged in an instance of the MessageCondition class.

In addition to routing messages to my program, I would like for my program to automatically start if it were not already running when the message is received. The MessageInterceptor class will take care of this for you if you enable application launching and give an ID string to identify your application. If your program is started due to an incoming SMS, you can retrieve the message by creating the MessageInterceptor using the ID string for your application, and will receive the message when the event handlers are attached to the object. The code for this is in the SmsMessageReceiver class. For my message rule, I am interested in any messages that contains the string "findme://".

void EnsureMessageInterceptor()
{
    if (_messageInterceptor == null)
    {
        //If the application launcher is enable then 
        //the system already has our message interception
        //rules. We recreate the message interceptor using our Application ID string
        if (MessageInterceptor.IsApplicationLauncherEnabled(ApplicationLaunchID))
        {
            _messageInterceptor = new MessageInterceptor(ApplicationLaunchID, 
                                      _session.UserSettings.EnableAutostart);
        }
        else
        {
            //The application launcher isn't enabled, so we need
            //to create a new interceptor and give it rules. 
            _messageInterceptor = new MessageInterceptor();
            MessageCondition messageCondition = 
              new MessageCondition(
                  MessageProperty.Body,
                  MessagePropertyComparisonType.Contains,"findme://"

                  );
            _messageInterceptor.InterceptionAction = 
                                InterceptionAction.NotifyAndDelete;
            _messageInterceptor.MessageCondition = messageCondition;
            
            //Enable application launching if the user's settings allow for it.
            if(_session.UserSettings.EnableAutostart)
               _messageInterceptor.EnableApplicationLauncher(ApplicationLaunchID);
        }                
    }
}

Once I have a message, I need to decide what to do with it. There are two types of messages that this program can receive: a user's location, or a request from another user to receive location. If a message contains a user's location, it will be packaged by appending the latitude, longitude, and possibly a comment immediately after the "findme://" string (findme://latitude,longitude,comment). If a user is requesting a location, then the "findme://" string will immediately be followed by "whereareyou". To perform the parsing for these messages, I used Regular Expressions.

static Regex LocationRegularExpression = 
  new  Regex(@"findme://(?<<atitude>(\-|\+)?\d+(\.\d*)?),"+
             "(?<longitude>(\-|\+)?\d+(\.\d*)?),(?<comment>.*)?", 
             RegexOptions.IgnoreCase|RegexOptions.Singleline);
static Regex LocationRequestRegularExpression = 
  new Regex(@"findme://whereareyou(\?format=(?<format>[a-z]*))?", 
            RegexOptions.IgnoreCase | RegexOptions.Singleline);

I can extract the information I need from a Regular Expression match by using the Groups collection on the Match result. In the following example code, I'm extracting the latitude and longitude received.

Match m = LocationRegularExpression.Match(smsMessage.Body);
if (m.Success)
{
    playAlert = true;
    double latitude = double.Parse(m.Groups["latitude"].Value);
    double longitude = double.Parse(m.Groups["longitude"].Value);
    string comment = m.Groups["Comment"].Value;
    
}

In the case of the findme://whereareyou message, an SMS will immediately be sent back using the findme://latitude,longitude,comment format. The code will also check for an optional format parameter after the message. The values for the format parameter can be any one of the enumeration values for LocationFormat (PlainText, LiveMapsLink, ShortLiveMapsLink, or FindMeString). If I wanted a link to a Microsoft Live Map that displays a user's location, I would send findme://whereareyou?format=LiveMapsLink. The program does not support sending requests like this, but the functionality is there if you want to access it.

Security

One would not want a program of this nature to report information without first considering whether or not the user of the application wishes to allow the requestor to see his or her position. The first time I implemented this program, there were two items that controlled whether or not the program would send a response. The first item was the requestor had to know the pin to send to the device. The second item was the user had to have granted permission for the requestor to see his or her position. I've eliminated the use of the pin, and now rely completely on contact permissions.

The Contact class has a member named Properties that can be used to store custom information on a user. If a contact is granted permission to see one's location, I save this information by adding a custom property onto the contact's entry and setting it equal to "Yes". The permission can be revoked by clearing out this value.

partial class ContactApproval: IContactApproval
{
 
    const string AllowLocationViewPropertyName = "Approved for Find Me";
    const string AllowLocationViewPropertyValue = "Yes";
 
    public void Allow(Contact c)
    {
        if (!c.Properties.Contains(AllowLocationViewPropertyName))
            c.Properties.Add(AllowLocationViewPropertyName);
        c.Properties[AllowLocationViewPropertyName] = 
            AllowLocationViewPropertyValue;
        c.Update();
    }
 
    public void Revoke(Contact c)
    {
        if (c.Properties.Contains(AllowLocationViewPropertyName))
            c.Properties[AllowLocationViewPropertyName] = null;
    }
 
    public bool IsAllowed(Contact c)
    {
        if ((c==null)||(c.ItemId.ToString().Equals("0")))
            return false;
        if (c.Properties.Contains(AllowLocationViewPropertyName))
        {
            object o = c.Properties[AllowLocationViewPropertyName];
            string allowSetting = o  as string;
            if (AllowLocationViewPropertyValue.Equals(allowSetting))
                return true;
        }
        return false;
    }
}

Logging a User's Location

The first thing the program does when a user's location is received is to save the location to the log. After the location is saved, it can be viewed by selecting "Friend's Location" from the program main menu. All of the information on this screen is pulled directly from the log with the exception of the Distance column. The distance is calculated based on one's current location. The equation used to calculate the distance is shown below:

public static double CalcDistance(double lat1, double lng1, double lat2, double lng2, 
              double radius /*radius of earth in the unit of your choosing*/)
{
 
    return radius * 2 * Math.Asin(Math.Min(1, Math.Sqrt(
       (Math.Pow(Math.Sin((DiffRadian(lat1, lat2)) / 2.0), 2.0) + 
        Math.Cos(ToRadian(lat1)) * Math.Cos(ToRadian(lat2)) * 
        Math.Pow(Math.Sin((DiffRadian(lng1, lng2)) / 2.0), 2.0)))));
}

When you select the option to view the user's location on the map, a pushpin is placed at both your location and the user's location. Depending on how far away you are, it may be necessary to zoom out to see both marks on the map.

Received Position

Future Editions

This is my first successful attempt at creating the WPSAPI wrapper, but not the last. The wrapper offers functionality that I've not yet used, and that functionality will need to be tested more. I would also like to make some changes to how memory is allocated, to reduce the number of garbage collection cycles that occur over the life of a session in which the wrapper is used. I'll be working on the wrapper more in the coming weeks, and plan to have the final version available in the Skyhook Wireless Discussion Group.

History

  • 6 January 2008 - Initial publication against Skyhook Wireless 27 SDK.
  • 9 January 2008 - Added WpsProxy as a content file.
  • 18 January 2008 - Updated code an article for newly released 3.0 SDK

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here