WP7 Code: Geofencing with the GeoLocation API and Rx

 

Besides providing geographical coordinates, computing distances, and making reverse geocoding service requests Windows Phone 7 can also signal when it is moved outside of a perimeter. The buzzword for this scenario is Geofencing, and this post shows how to implement it (when the geofence is circular) with the WP7 GeoLocation API and Rx.

An utilitarian UI will do just fine for a first exploration of geofencing. The UI starts from the default Windows Phone Application template and adds 6 textblocks (some of which are static labels), a slider, and a button inside the ContentPanel. The code follows below, with the portion added to the default template in green:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <TextBlock Height="54" HorizontalAlignment="Left" Margin="14,38,0,0" Name="textBlock1" Text="Here" VerticalAlignment="Top" Width="426" />
<TextBlock Height="54" HorizontalAlignment="Left" Margin="14,98,0,0" Name="hereTextBlock" Text="(none)" VerticalAlignment="Top" Width="426" />
<TextBlock Height="54" HorizontalAlignment="Left" Margin="14,158,0,0" Name="textBlock3" Text="Perimeter center" VerticalAlignment="Top" Width="426" />
<TextBlock Height="54" HorizontalAlignment="Left" Margin="14,218,0,0" Name="centerTextBlock" Text="(none)" VerticalAlignment="Top" Width="426" />
<TextBlock Height="54" HorizontalAlignment="Left" Margin="14,278,0,0" Name="fenceTextBlock" Text="Perimeter radius" VerticalAlignment="Top" Width="426" />
<TextBlock Height="54" HorizontalAlignment="Left" Margin="24,438,0,0" Name="distanceTextBlock" Text="(none)" VerticalAlignment="Top" Width="426" />
<Button Content="Set Center" Height="90" HorizontalAlignment="Left" Margin="92,515,0,0" Name="geoFenceButton" VerticalAlignment="Top" Width="269" />
<Slider Height="101" HorizontalAlignment="Left" Margin="24,308,0,0" Name="slider1" VerticalAlignment="Top" Width="381" Minimum="10" Maximum="1000" />
</Grid>

Next add references to System.Devices.Sensors (for GeoLocation), and System.Observable and Microsoft.Phone.Reactive (for Rx). I will also be using the GeoLocation library from my previous post so add a Project reference as well.

For a (slight) change of style I’ll expose the GeoCoordinateWatcher instance as a property of the MainPage class. Here’s the relevant code:

public partial class MainPage : PhoneApplicationPage
{

    private GeoCoordinateWatcher gcw;
public GeoCoordinateWatcher Gcw
{
get
{
if (gcw == null)
gcw = new GeoCoordinateWatcher { MovementThreshold = 10.0 };
return gcw;
}
}
    // snip

The page navigation overrides (To and From) start and stop the geolocation subsystem:

protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    Gcw.Start();
}

protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
{
    base.OnNavigatedFrom(e);

    Gcw.Stop();
}

The code begins by disabling the button and slider controls. A first Rx query provides a stream of location events by joining the geolocation status changed event stream with the position changed event stream, and filtering out readings with horizaontal accuracy greater than 100m (below). Note that since Rx lazily evaluates the query, building it on the startup critical path (i.e., the MainPage constructor) doesn’t really do anything besides getting the query in place.

geoFenceButton.IsEnabled = false;
slider1.IsEnabled = false;

var positionUpdates = from sc in Gcw.GetStatusChangedEventStream()
                      where sc.Status == GeoPositionStatus.Ready
                      from pos in Gcw.GetPositionChangedEventStream()
                      let location = pos.Position.Location
                      where (!Double.IsNaN(location.Latitude) && !Double.IsNaN(location.Longitude) && location.HorizontalAccuracy <= 100.0)
                      select location;

As soon as the position updates start trickling in the code enables the button (for the first position update via the Take operator), and displays the current location (lat/long) in the appropriate text box. Here are the subscribers for these event streams:

positionUpdates.Take(1).Subscribe(_ => geoFenceButton.IsEnabled = true);

positionUpdates.Subscribe(l =>
{
    this.hereTextBlock.Text = string.Format( "{0:###.00000000N;###.00000000S;0}, {1:###.00000000E;###.00000000W;0}"
                                           , l.Latitude
                                           , l.Longitude
                                           );
});

Tapping the button causes the application to use the current position as the perimeter’s center—hence enabling the button only after the position becomes available. This requires bringing button Click events into the realm of Rx, and then a query that for each tap pulls the most recent position. The query updates the appropriate text box with the center’s position. The code is below, with the extension method first (put it in a static Helpers class) followed by the event stream code (put it into MainPage). Just like the code above, the first button click event enables additional UI elements—in this case the slider control—and sets their initial value to 50.

public static IObservable<RoutedEventArgs> GetClickEventStream(this Button button)
{
    return Observable.Create<RoutedEventArgs>(observable =>
    {
        RoutedEventHandler handler = (s, e) =>
        {
            observable.OnNext(e);
        };
        button.Click += handler;
        return () => { button.Click -= handler; };
    });
}

var clicks = this.geoFenceButton.GetClickEventStream();

            clicks.Take(1).Subscribe(_ =>
                {
                    this.slider1.IsEnabled = true;
                    this.slider1.Value = 50.0;
                });

            var centers = positionUpdates.Publish(updates => from position in updates
                                                             from click in clicks.Take(1).TakeUntil(updates)
                                                             select position);

            centers.Subscribe(center =>
                {
                    centerTextBlock.Text = string.Format("{0:###.00000000N;###.00000000S;0}, {1:###.00000000E;###.00000000W;0}", center.Latitude, center.Longitude);
                }
                );

The slider determines the perimeter’s radius. As with other UI controls such as the button, the events it raises can be used in Rx queries. Here’s the extension method that does the bridging between RoutedPropertyChangedEventArgs<double> and IObservable<RoutedPropertyChangedEventArgs<double>> (and goes in the Helpers class), followed by the code that updates the UI element with the radius selected via the slider (and goes in the MainPage constructor):

public static IObservable<RoutedPropertyChangedEventArgs<double>> GetValueChangedEventStream(this Slider slider)
{
    return Observable.Create<RoutedPropertyChangedEventArgs<double>>(observable =>
        {
            RoutedPropertyChangedEventHandler<double> handler = (s,e) =>
                {
                    observable.OnNext(e);
                };
            slider.ValueChanged += handler;
            return () => { slider.ValueChanged -= handler; };
        });
}

var radii = this.slider1.GetValueChangedEventStream();

radii.Subscribe(radius =>
    {
        this.fenceTextBlock.Text = string.Format("Perimeter radius {0:####.00}m",radius.NewValue);
    });

The last two queries compute the distance from the perimeter’s center, and the distance to the fence. The first query uses the CombineLatest operator to return the (Haversine) distance from the center to the current position. Note that I use the GetDistanceTo method rather than the homegrown version shown in the previous posts—thanks for the feedback! The second query computes the distance to the fence by subtracting the latest distance from center from the latest radius value from the slider. The subscriber to this last stream updates the appropriate text box with a message, changing the text color depending on where the current location lies with respect to the fence. Here’s the code:

var distancesFromCenter = positionUpdates.CombineLatest(centers, (p, c) => p.GetDistanceTo(c)).DistinctUntilChanged();

var distancesToFence = distancesFromCenter.CombineLatest(this.slider1.GetValueChangedEventStream(), (d, radius) => radius.NewValue - d);
distancesToFence.Subscribe(distanceToFence =>
    {
        var msg = string.Empty;
        if (distanceToFence >= 0)
        {
            msg = string.Format("{0:####.00}m from the fence", this.slider1.Value - distanceToFence);
            distanceTextBlock.Foreground = new SolidColorBrush(Colors.Green);
        }
        else
        {
             msg = string.Format("{0:####.00}m outside the fenced perimeter", Math.Abs(distanceToFence));
             distanceTextBlock.Foreground = new SolidColorBrush(Colors.Red);
        }
        this.distanceTextBlock.Text = msg;
    });

The code for MainPage.xaml.cs follows. To build and run the application you’ll need the XAML as well as the GeoLocation library from my reverse Geocoding post.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Reactive;
using System.Device.Location;
using GeoLocation;

namespace GeoFencing
{
    public partial class MainPage : PhoneApplicationPage
    {

        private GeoCoordinateWatcher gcw;
        public GeoCoordinateWatcher Gcw
        {
            get
            {
                if (gcw == null)
                    gcw = new GeoCoordinateWatcher { MovementThreshold = 10.0 };
                return gcw;
            }
        }
        // snip

        // Constructor
        public MainPage()
        {
            InitializeComponent();

            geoFenceButton.IsEnabled = false;
            slider1.IsEnabled = false;

            var positionUpdates = from sc in Gcw.GetStatusChangedEventStream()
                                  where sc.Status == GeoPositionStatus.Ready
                                  from pos in Gcw.GetPositionChangedEventStream()
                                  let location = pos.Position.Location
                                  where (!Double.IsNaN(location.Latitude) && !Double.IsNaN(location.Longitude) && location.HorizontalAccuracy <= 100.0)
                                  select location;

            positionUpdates.Take(1).Subscribe(_ => geoFenceButton.IsEnabled = true);

            positionUpdates.Subscribe(l =>
            {
                this.hereTextBlock.Text = string.Format( "{0:###.00000000N;###.00000000S;0}, {1:###.00000000E;###.00000000W;0}"
                                                       , l.Latitude
                                                       , l.Longitude
                                                       );
            });

            var clicks = this.geoFenceButton.GetClickEventStream();

            clicks.Take(1).Subscribe(_ =>
                {
                    this.slider1.IsEnabled = true;
                    this.slider1.Value = 50.0;
                });

            var centers = positionUpdates.Publish(updates => from position in updates
                                                             from click in clicks.Take(1).TakeUntil(updates)
                                                             select position);

            centers.Subscribe(center =>
                {
                    centerTextBlock.Text = string.Format("{0:###.00000000N;###.00000000S;0}, {1:###.00000000E;###.00000000W;0}", center.Latitude, center.Longitude);
                }
                );

            var radii = this.slider1.GetValueChangedEventStream();

            radii.Subscribe(radius =>
                {
                    this.fenceTextBlock.Text = string.Format("Perimeter radius {0:####.00}m",radius.NewValue);
                });

            var distancesFromCenter = positionUpdates.CombineLatest(centers, (p, c) => p.GetDistanceTo(c)).DistinctUntilChanged();

            var distancesToFence = distancesFromCenter.CombineLatest(this.slider1.GetValueChangedEventStream(), (d, radius) => radius.NewValue - d);
            distancesToFence.Subscribe(distanceToFence =>
                {
                    var msg = string.Empty;
                    if (distanceToFence >= 0)
                    {
                        msg = string.Format("{0:####.00}m from the fence", this.slider1.Value - distanceToFence);
                        distanceTextBlock.Foreground = new SolidColorBrush(Colors.Green);
                    }
                    else
                    {
                         msg = string.Format("{0:####.00}m outside the fenced perimeter", Math.Abs(distanceToFence));
                         distanceTextBlock.Foreground = new SolidColorBrush(Colors.Red);
                    }
                    this.distanceTextBlock.Text = msg;
                });
        }

        protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);

            Gcw.Start();
        }

        protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);

            Gcw.Stop();
        }

static class Helpers
    {
        public static IObservable<RoutedEventArgs> GetClickEventStream(this Button button)
        {
            return Observable.Create<RoutedEventArgs>(observable =>
            {
                RoutedEventHandler handler = (s, e) =>
                {
                    observable.OnNext(e);
                };
                button.Click += handler;
                return () => { button.Click -= handler; };
            });
        }

        public static IObservable<RoutedPropertyChangedEventArgs<double>> GetValueChangedEventStream(this Slider slider)
        {
            return Observable.Create<RoutedPropertyChangedEventArgs<double>>(observable =>
                {
                    RoutedPropertyChangedEventHandler<double> handler = (s,e) =>
                        {
                            observable.OnNext(e);
                        };
                    slider.ValueChanged += handler;
                    return () => { slider.ValueChanged -= handler; };
                });
        }
    }
}

Here’s a screenshot showing the phone within the geofenced perimeter:

image

In summary, this post has shown:

  • How to perform computations on two event streams using the CombineLatest operator
  • How to use Rx with a slider and button control
  • How to implement geofencing using Windows Phone 7’s geolocation API and Rx queries