Using the Location Service in Silverlight for Windows Phone

7 Comments January 25, 2011

One of the most exciting features of the Windows phone from a developer’s perspective is the location service. The location service is a set of APIs that rely on Assisted-GPS (A-GPS), Wi-Fi Positioning System (WPS), and cell-site triangulation to expose location data to an application. Simply put, this means that a Windows phone app can determine where it’s at at almost any time, excepting situations where GPS satellites aren’t reachable, cell towers aren’t in range, and no Wi-Fi signals are available.  Combined with Microsoft’s Bing Maps Silverlight control for Windows Phone, the location service opens the door to an entire genre of apps that wouldn’t be possible otherwise.

The core of the location API is embodied in a class named GeoCoordinateWatcher, which lives in the System.Device.Location namespace of the System.Device assembly. The first thing you do to build a phone app that uses location is add a reference to System.Device to your project. Then you create an instance of GeoCoordinateWatcher, like this:

 

GeoCoordinateWatcher watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.High);

 

The value passed to the constructor specifies the accuracy that you desire in your location data. The two possible values are GeoPositionAccuracy.Default and GeoPositionAccurary.High. According to the documentation, GeoPositionAccuracy.Default means “Optimize for power, performance, and other cost considerations,” while GeoPositionAccuracy.High means “Deliver the most accurate report possible. This includes using services that might charge money, or consuming higher levels of battery power or connection bandwidth.” Translated, this means that Default will ignore GPS even if it’s available, trading accuracy for lower power consumption, while High will attempt to use GPS to deliver the most accurate data possible at the expense of increased power consumption.

The next step after instantiating a GeoCoordinateWatcher object is to set its MovementThreshold property:

 

watcher.MovementThreshold = 20; // 20 meters

 

Once more the documentation is a bit vague about what this means, but a simple way to think about it is that the lower the MovementThreshold, the more often your app will be pinged with location data. The downside to lower values is increased power consumption. You can set MovementThreshold to 0 if you’d like, but Microsoft’s Location Programming Best Practices for Windows Phone recommends a minimum setting of 20 meters. If you’re building a navigation app to be used while driving, 20 meters is a reasonable setting because a car can cover 20 meters in a hurry. But if you’re building an app to track the location of someone running or walking, you might want to set MovementThreshold to something lower.

The next step is to register handlers for GeoCoordinateWatcher’s StatusChanged and PositionChanged events:

  

watcher.StatusChanged +=

    new EventHandler<GeoPositionStatusChangedEventArgs>(OnStatusChanged);

watcher.PositionChanged +=

    new EventHandler<GeoPositionChangedEventArgs<GeoCoordinate>>(OnPositionChanged);

 

StatusChanged fires whenever the location status changes – for example, when the location service acquires or loses your position. PositionChanged events occur whenever your position changes, and their frequency is determined by 1) the value of the MovementThreshold property, and 2) how fast you’re traveling. In my experience, PositionChanged events never fire faster than about once per second, but if you set MovementThreshold to 20 and then stand in place, one or two PositionChanged events will fire and then won’t fire again until you’ve moved 20 meters or so.

Once the event handlers are registered, you start the location service by calling GeoCoordinateWatcher.Start:

 

watcher.Start();

 

You’ll receive a StatusChanged event almost immediately indicating that the service is initializing, and shortly thereafter you’ll receive another StatusChanged event telling you that the location service is “Ready” (assuming, of course, it was able to acquire a source for location data). If you have a cell signal or are connected to a wireless network, the Ready signal will typically come within a second or two. If the location service must wait for a GPS lock, however, it may be a minute or more before the service says it’s ready.

Shortly after you call GeoCoordinateWatcher.Start, you’ll start receiving PositionChanged events if indeed the location service was able to find a source of location data.  Each PositionChanged event is accompanied by a GeoPositionChangedEventArgs which exposes properties named Position and Timestamp. The Position property exposes a subproperty named Location. Through the Location property, you can access these properties:

  • Latitude, which reveals your current latitude
  • Longitude, which reveals your current longitude
  • Altitude, which reveals your current altitude in meters
  • Course, which indicates which direction you’re traveling in 360-degree compass coordinates (0=North, 90=East, 180=South, and 270=West)
  • Speed, which indicates how fast you’re traveling in meters per second
  • HorizontalAccuracy, which specifies the horizontal accuracy in meters
  • VerticalAccuracy, which specifies the vertical accuracy in meters

Keep in mind that the values of any of these properties can be Double.Nan, which means no data is currently available for that property. For example, altitude can only be determined through GPS, so if the location service is currently getting location data by triangulating cell signals, Latitude and Longitude will contain valid (though not terribly accurate) values, but altitude will be “Not a Number.” That could change in the next PositionChanged event if the location service manages to acquire a GPS lock in the meantime.

There is no property or method that you can call to determine how the location service is getting its data, but you can infer whether the data is coming from GPS by checking the HorizontalAccuracy and VerticalAccuracy properties. When the phone has a GPS connection, these values will typically fall between 4 and 7 meters. When the location data comes from non-GPS sources, however, HorizontalAccuracy and VerticalAccuracy can be 100 meters or more – or even NaN.

Because the location service’s status can change often, especially if you’re driving down the interstate going under bridges and going in and out of contact with cell towers, it’s useful to process StatusChanged events to let the user know what’s going on. StatusChanged events are accompanied by GeoPositionStatusChangedEventArgs whose Status property can be any one of four values:

  • GeoPositionStatus.Disabled, which indicates that the location service has been disabled by the user
  • GeoPositionStatus.Initializing, which indicates that the location service is trying to connect to a source of data
  • GeoPositionStatus.Ready, which indicates that the location service is receiving location data
  • GeoPositionStatus.NoData, which indicates that although the location service is enabled, no location data is currently available

If you receive a StatusChanged event indicating that the location service is disabled, you might want to pop up a message box informing the user that they’ll need to go into their phone’s settings and enable the location feature. On the other hand, if the service is up and running but suddenly loses contact with the source (or sources) of location data, you may want to let the user know by responding to StatusChanged events with a status of GeoPositionStatus.NoData.

You could easily write an app that displays a textual readout of the data emanating from a GeoCoordinateWatcher, but that would be boring. Which is one reason why Silverlight for Windows Phone includes a handy Map control. The Map control lives in the Microsoft.Phone.Controls.Maps namespace of the Microsoft.Phone.Controls.Maps assembly. Once you’ve added a reference to that assembly to your project, declaring a Map control is no more difficult than this:

 

<maps:Map x:Name="BingMap" CopyrightVisibility="Collapsed"

  ZoomBarVisibility="Visible" ZoomLevel="17" Mode="AerialWithLabels"

  CredentialsProvider="..." Center="47.6741667,-122.1202778" />

 

The Map control’s Center property centers the map at the specified latitude and longitude. Rather than hard-code this value, you could get the current location from the location service and assign it to Center. Better yet, pass the Map a new latitude and longitude every time GeoCoordinateWatcher fires a PositionChanged event, and the map will follow you as you move around.

Before you can use the Map control, you must go to the Bing Maps Account Center and obtain an API key. It’s free and it’s fast. The key is a long string of letters and numbers that you assign to the control’s CredentialsProvider property. The API key comes with a few restrictions, but in general, most apps – even apps that are purchased rather than free – can use it without charge. You can review the terms of use online.

Want pushpins to pinpoint locations on the map? That’s easy, too:

 

<maps:Map x:Name="BingMap" CopyrightVisibility="Collapsed"

  ZoomBarVisibility="Visible" ZoomLevel="17" Mode="AerialWithLabels"

  CredentialsProvider="..." Center="47.6741667,-122.1202778">

  <maps:Pushpin x:Name="Pin" Background="Red" />

</maps:Map>

 

By default, a pushpin renders itself as a 4-sided polygon that somewhat resembles a flag. Because Pushpin is a content control, you can stuff arbitrary content inside it by assigning that content to the Content property. Or you can reshape the pushpin by retemplating it. The following XAML declares a pushpin and retemplates it so it appears as a red ellipse:

 

<ControlTemplate x:Key="PushpinTemplate" TargetType="maps:Pushpin">

  <Grid>

    <Ellipse Width="50" Height="50" Fill="Red" />

    <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />

  </Grid>

</ControlTemplate>

  .

  .

  .

<maps:Pushpin x:Name="Pin" Template="{StaticResource PushpinTemplate}" />

 

I built an app that combines the location service with a Map control to show where you are – and which direction you’re headed – in real time. When the app starts up, it initializes a GeoCoordinateWatcher. When the location data starts flowing, the app shows your current location with a templated pushpin that looks like an arrow. And when a PositionChanged event containing a non-NaN Course property arrives (that’ll happen after you’ve moved a block or two), the arrow rotates to show your direction of travel. Although not shown in the screen capture below, the display also includes a small textual readout revealing the horizontal and vertical accuracies reported in the PositionChanged events. Initially these values will be pretty high (or NaN), but if you move outside so the phone can acquire a GPS lock, you’ll see the numbers drop down to something more reasonable like 4 meters.

screen

The source code is shown below. I set GeoCoordinateWatcher’s MovementThreshold to 10 meters, so if you’re driving, you’ll receive map updates once a second or so, but if you’re walking, updates will be less frequent. You can lower the threshold to generate more frequent updates, but remember that lower MovementThresholds consume battery power faster. A fun addition to the app would be a slider that allows the user to set the MovementThreshold. That way, a user who is driving could select a rather high value, while a user out for a morning run could select something finer.

 

// MainPage.xaml

<Grid x:Name="LayoutRoot" Background="Transparent">

    <Grid.Resources>

        <ControlTemplate x:Key="PushpinTemplate" TargetType="maps:Pushpin">

            <Grid RenderTransformOrigin="0.5,0.5">

                <Grid.RenderTransform>

                    <RotateTransform />

                </Grid.RenderTransform>

                <Polygon Fill="Red" Points="15,0 0,60 30,60" />

                <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />

            </Grid>

        </ControlTemplate>

    </Grid.Resources>

       

    <!--ContentPanel - place additional content here-->

    <Grid x:Name="ContentPanel">

        <maps:Map x:Name="BingMap" CopyrightVisibility="Collapsed"

            ZoomBarVisibility="Visible" ZoomLevel="17" Mode="AerialWithLabels"

            CredentialsProvider="...">

            <maps:Pushpin x:Name="Pin" PositionOrigin="0.5,0.5"

                Template="{StaticResource PushpinTemplate}" />

        </maps:Map>

        <TextBlock x:Name="Output" Foreground="White" FontSize="24"

            HorizontalAlignment="Center" VerticalAlignment="Top" />

    </Grid>

</Grid>

 

// MainPage.xaml.cs

private RotateTransform _transform;

private GeoCoordinateWatcher _watcher;

 

// Constructor

public MainPage()

{

    InitializeComponent();

 

    // Create and configure a GeoCoordinateWatcher

    _watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.High);

    _watcher.MovementThreshold = 10; // 10 meters

    _watcher.StatusChanged +=

        new EventHandler<GeoPositionStatusChangedEventArgs>(OnStatusChanged);

    _watcher.PositionChanged +=

        new EventHandler<GeoPositionChangedEventArgs<GeoCoordinate>>(OnPositionChanged);

    _watcher.Start();

}

 

void OnStatusChanged(object sender, GeoPositionStatusChangedEventArgs e)

{

    if (e.Status == GeoPositionStatus.Disabled)

        MessageBox.Show("The location service is currently turned off.");

    else if (e.Status == GeoPositionStatus.NoData)

        MessageBox.Show("No location data is currently available. Try again later.");

}

 

void OnPositionChanged(object sender, GeoPositionChangedEventArgs<GeoCoordinate> e)

{

    // Move the pushpin to the current location and recenter the map

    Pin.Location = new GeoCoordinate(e.Position.Location.Latitude,

        e.Position.Location.Longitude);

    BingMap.Center = new GeoCoordinate(e.Position.Location.Latitude,

        e.Position.Location.Longitude);

 

    // Update onscreen accuracy readout

    Output.Text = String.Format("Acc Horz = {0:0.00} | Acc Vert = {1:0.00}",

        e.Position.Location.HorizontalAccuracy, e.Position.Location.VerticalAccuracy);

 

    // Update direction by rotating the pushpin

    if (!double.IsNaN(e.Position.Location.Course))

    {

        if (_transform == null)

        {

            foreach (DependencyObject visual in GetVisualElements(Pin))

            {

                if (visual is Grid)

                {

                    _transform = (visual as Grid).RenderTransform as RotateTransform;

                    break;

                }

            }

        }

 

        if (_transform != null)

            _transform.Angle = e.Position.Location.Course;

    }

}

 

private IEnumerable<DependencyObject> GetVisualElements(DependencyObject root)

{

    int count = VisualTreeHelper.GetChildrenCount(root);

 

    for (int i = 0; i < count; i++)

    {

        var child = VisualTreeHelper.GetChild(root, i);

        yield return child;

 

        foreach (var descendant in GetVisualElements(child))

        {

            yield return descendant;

        }

    }

}

 

Note how I used VisualTreeHelper to find the RotateTransform assigned to the control template for the pushpin. One of the nuances of retemplating controls in Silverlight (and Silverlight for Windows Phone) is that you can’t reference named elements in a template the same way you reference named elements elsewhere in the XAML. Instead, you use VisualTreeHelper to examine the elements generated from the template and walk the tree from top to bottom until you find the element you’re looking for. This is standard practice in Silverlight apps, and it works in Silverlight for Windows Phone, too.

You can download the source code and try the app for yourself. Take a drive around the block and watch the map update as you do. (I did, and I did so legally because Tennessee has no laws against programming while driving.) The only caveat is that before you can use the app, you’ll need to plug your own Bing Maps API key into CredentialsProvider. I considered including mine, but since I’m disseminating the source code, I didn’t want to hand out my API key, too. Enjoy!


7 Comments

  • Gravatar Image
    Twitter Trackbacks for Jeff Prosise's Blog : Using the Location Service in Silverlight for Windows Phone [wintellect.com] on Topsy.com January 25, 2011 9:39 AM

    PingBack from http://topsy.com/www.wintellect.com/CS/blogs/jprosise/archive/2011/01/25/using-the-location-service-in-silverlight-for-windows-phone.aspx?utm_source=pingback&utm_campaign=L2

  • Gravatar Image
    kbcoder January 27, 2011 11:49 PM

    Great tutorial and works really nicely.

  • Gravatar Image
    marcus p March 9, 2011 11:34 AM

    Great Tutorial - very helpful.
    Thank you very much!

    Kind regards,

    Marcus

  • Gravatar Image
    gurpreet singh August 24, 2011 5:45 AM

    very well explained , thankx !

  • Gravatar Image
    satish October 24, 2011 1:44 PM

    Gereat Tutorial for beginers like me.
    Thank you very much.

    Regards,
    Satish

  • Gravatar Image
    Luxoli November 26, 2012 9:08 AM

    You sir, are a star. Thank you very much

  • Gravatar Image
    Shoaib January 9, 2013 4:42 AM

    This is by far the best tutorial I've read about WP7 GPS (and boy have I read a lot). Absolutely awesome!!! This will attract more customers for me :-). Thanks a million.

Have a Comment?

Archives

Tags