Geofencing and Universal Windows 10 Apps

Geofencing and Universal Windows 10 Apps

GeoWin10

Short introduction

In this post I would like to share my experience with geofencing and Universal Windows 10 Apps.

First of all we need answer one question – what geofencing is?

Geo-fencing (geofencing) is a feature in a software program that uses the global positioning system (GPS) or radio frequency identification (RFID) to define geographical boundaries. A geofence is a virtual barrier.

To sum up geofence allows to detect user location and react somehow when user is in the selected place.

Let’s  see how does it work with Universal Windows 10 App.

What do I need to start?

1) Visual Studio 2015 Community (for free) or higher

2) Smartphone with Windows 10 Mobile system

3) Bing map key generated ( you can create it here)

Let’s start

 1) Launch Visual Studio and create new empty project – call it as you wish:

a) Structure of your project should look like below:

  • Assets folder
  • Classes folder
  • ContentDialogs folder
  • Pages folder

Geo1

Geo3

 2) Add “ExtendedGeofence” class to Classes folder:

Please note that in below fragment there is explanation added as a comment. Read them first and then we can move forward.

This class is used for creating geofences.

class ExtendedGeofenceFactory
    {
        private double _radius;

        private Geocircle _geocircle;

        private Geofence _geofence;

        private bool _singleUse;

        private MonitoredGeofenceStates _monitoredStates;

        private TimeSpan _dwellTime;

        private TimeSpan _duration;

        private DateTimeOffset _startTime;

        private string _fenceId;

        public Geofence CreateGeofence(string fenceID, double latitude, double longitude, double altitude, double radius, bool singleUse, int dwellTime, int duration)
        {
            _fenceId = fenceID;
            // Define the fence location and radius.
            BasicGeoposition position;
            position.Latitude = latitude;
            position.Longitude = longitude;
            position.Altitude = altitude;
            _radius = radius; // in meters

            // Set the circular region for geofence.
            _geocircle = new Geocircle(position, radius);

            // Remove the geofence after the first trigger.
            _singleUse = singleUse;

            // Set the monitored states.
            _monitoredStates = MonitoredGeofenceStates.Entered | MonitoredGeofenceStates.Exited | MonitoredGeofenceStates.Removed;

            // Set how long you need to be in geofence for the enter event to fire.
            _dwellTime = TimeSpan.FromSeconds(dwellTime);

            // Set how long the geofence should be active.
            _duration = TimeSpan.FromDays(duration);

            // Set up the start time of the geofence.
            _startTime = DateTime.Now;

            // Create the geofence.
            _geofence = new Geofence(_fenceId, _geocircle, _monitoredStates, _singleUse, _dwellTime, _startTime, _duration);
            return _geofence;
        }
    }

 3) Add “PlaceContentDialog” content dialog to ContentDialogs folder:

This content dialog will be shown on the screen when user enters selected geofence.

Replace xaml code with one below. Also note that I have added xaml code responsible for displaying rounded image.

<ContentDialog x:Class="PlacesGeofencing.ContentDialogs.PlaceContentDialog" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:PlacesGeofencing.ContentDialogs" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" xmlns:controls="using:WinRTXamlToolkit.Controls" FullSizeDesired="True" Background="Transparent">
    <ContentDialog.Resources>




<Style TargetType="Button" x:Name="MyButtonStyle">
            <Setter Property="Background" Value="#FF1F3B53"/>
            <Setter Property="Foreground" Value="#FF000000"/>
            <Setter Property="Padding" Value="3"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="BorderBrush">
                <Setter.Value>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="#FFA3AEB9" Offset="0"/>
                        <GradientStop Color="#FF8399A9" Offset="0.375"/>
                        <GradientStop Color="#FF718597" Offset="0.375"/>
                        <GradientStop Color="#FF617584" Offset="1"/>
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="Button">
                        <Grid>
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal"/>
                                    <VisualState x:Name="MouseOver">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="BackgroundAnimation" Storyboard.TargetProperty="Opacity" To="1"/>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" To="#F2FFFFFF"/>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" To="#CCFFFFFF"/>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" To="#7FFFFFFF"/>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Pressed">
                                        <Storyboard>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="Background" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" To="#40C4FF"/>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="BackgroundAnimation" Storyboard.TargetProperty="Opacity" To="1"/>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)" To="#40C4FF"/>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" To="#40C4FF"/>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" To="#40C4FF"/>
                                            <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" To="#40C4FF"/>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Disabled">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="DisabledVisualElement" Storyboard.TargetProperty="Opacity" To=".55"/>
                                        </Storyboard>
                                    </VisualState>
                                </VisualStateGroup>
                                <VisualStateGroup x:Name="FocusStates">
                                    <VisualState x:Name="Focused">
                                        <Storyboard>
                                            <DoubleAnimation Duration="0" Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Opacity" To="1"/>
                                        </Storyboard>
                                    </VisualState>
                                    <VisualState x:Name="Unfocused" />
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                            <Border x:Name="Background" CornerRadius="4" Background="Transparent" BorderThickness="2" BorderBrush="#FFFFFFFF">
                                <Grid Background="Transparent">
                                    <Border Opacity="0" x:Name="BackgroundAnimation" Background="Transparent"/>
                                    <Rectangle x:Name="BackgroundGradient" >
                                        <Rectangle.Fill>
                                            <LinearGradientBrush StartPoint=".7,0" EndPoint=".7,1">
                                                <GradientStop Color="Transparent" Offset="0" />
                                                <GradientStop Color="Transparent" Offset="0.375" />
                                                <GradientStop Color="Transparent" Offset="0.625" />
                                                <GradientStop Color="Transparent" Offset="1" />
                                            </LinearGradientBrush>
                                        </Rectangle.Fill>
                                    </Rectangle>
                                </Grid>
                            </Border>
                            <ContentPresenter x:Name="contentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="0,8,0,10"/>
                            <Rectangle x:Name="DisabledVisualElement" RadiusX="4" RadiusY="4" Fill="#FFFFFFFF" Opacity="0" IsHitTestVisible="false" />
                            <Rectangle x:Name="FocusVisualElement" RadiusX="4" RadiusY="4" Margin="0" Stroke="#FF6DBDD1" StrokeThickness="1" Opacity="0" IsHitTestVisible="false" />
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>




    </ContentDialog.Resources>

    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="0,20,0,0">
        <StackPanel Orientation="Vertical" HorizontalAlignment="Stretch">
            <StackPanel Orientation="Vertical">
                <Ellipse Width="80" Height="80">
                    <Ellipse.Fill>
                        <ImageBrush>
                            <ImageBrush.ImageSource>
                                <BitmapImage x:Name="personBitmapImage" UriSource="ms-appx:///Assets/windows10.png" />
                            </ImageBrush.ImageSource>
                        </ImageBrush>
                    </Ellipse.Fill>
                </Ellipse>
                <TextBlock x:Name="PlaceNameTextBlock" Text="Title" Foreground="White" HorizontalAlignment="Center" FontWeight="Bold" Margin="0,10,0,0" FontSize="18.667"/>
            </StackPanel>

            <StackPanel Orientation="Vertical" Margin="0,30,0,0">
                <TextBlock Text="Description:" Foreground="White" FontWeight="Bold" FontSize="16" VerticalAlignment="Center"/>
                <StackPanel Orientation="Horizontal" Margin="0,4,0,0"> 
                    <TextBlock x:Name="PlaceDescriptionTextBlock" Text="" TextWrapping="Wrap" Foreground="White" FontSize="12"/>
                </StackPanel>
            </StackPanel>

        </StackPanel>
    </Grid>

</ContentDialog>

Paste below code in “PlaceContentDialog.xaml.cs” file:

public sealed partial class PlaceContentDialog : ContentDialog
    {
        private string placeName;
        public string PlaceName
        {
            get
            {
                return placeName;
            }
            set
            {
                if (string.IsNullOrEmpty(value))
                    throw new ArgumentNullException();

                else
                {
                    placeName = value;
                    PlaceNameTextBlock.Text = placeName;
                }
            }
        }

        private string placeDescription;
        public string PlaceDescription
        {
            get
            {
                return placeDescription;
            }
            set
            {
                if (string.IsNullOrEmpty(value))
                    throw new ArgumentNullException();

                else
                {
                    placeDescription = value;
                    PlaceDescriptionTextBlock.Text = placeDescription;
                }
            }
        }

        public PlaceContentDialog()
        {
            InitializeComponent();
        }

        public PlaceContentDialog(string personName, string aboutPerson)
        {
            InitializeComponent();
            setContentDialogData(personName, aboutPerson);
        }

        private void setContentDialogData(string personName, string aboutPerson)
        {
            PlaceName = personName;
            PlaceDescription = aboutPerson;
        }
    }

 

4) Add “PlacesMapPage” page to Pages folder:

This view is the most important. In this page map is displayed with user current position.

I have added two image assets (do not worry I have included them to project published on Github).

a) LocationPin:

LocationPin

b) Windows10Logo:

windows10Logo

Here is the XAML page:

<Page x:Class="PlacesGeofencing.Pages.PlacesMapPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:PlacesGeofencing.Pages" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:maps="using:Windows.UI.Xaml.Controls.Maps" mc:Ignorable="d">

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<maps:MapControl x:Name="MessageMapControl" MapServiceToken="<YOUR MAP TOKEN HERE" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" ZoomLevel="16"/>
</Grid>
</Page>

Paste below code in “PlacesMapPage.xaml.cs” file. I have added comments to the code so you can understand how to add geofences and how to react when user enters into one.

public sealed partial class PlacesMapPage : Page
    {
        IList<PlaceContentDialog> _dialogsList;
        IList<Geofence> _geofences;
        MapIcon _userLocationIcon;
        public PlacesMapPage()
        {
            InitializeComponent();
        }

        protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
        {
            base.OnNavigatingFrom(e);
            GeofenceMonitor.Current.GeofenceStateChanged -= OnGeofenceStateChanged;
            GeofenceMonitor.Current.StatusChanged -= OnGeofenceStatusChanged;
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
            requestLocationAccess();
            _dialogsList = new List<PlaceContentDialog>();
        }

        private async void requestLocationAccess()
        {
            var accessStatus = await Geolocator.RequestAccessAsync();

            switch (accessStatus)
            {
                case GeolocationAccessStatus.Allowed:
                    // Create Geolocator and define perodic-based tracking (2 second interval).
                    System.Diagnostics.Debug.WriteLine("Access to location is allowed.");
                    configureGeofenceMonitor();
                    getCurrentLocation();
                    break;

                case GeolocationAccessStatus.Denied:
                    System.Diagnostics.Debug.WriteLine("Access to location is denied.");
                    break;

                case GeolocationAccessStatus.Unspecified:
                    System.Diagnostics.Debug.WriteLine("Unspecificed error!");
                    break;
            }
        }

        private void getCurrentLocation()
        {
            Geolocator _geolocator = new Geolocator { ReportInterval = 2000 };

            // Subscribe to the PositionChanged event to get location updates.
            _geolocator.PositionChanged += (s, e) =>
            {
                System.Diagnostics.Debug.WriteLine("Position changed: " + e.Position.Coordinate.Point.Position.Latitude + " " + e.Position.Coordinate.Point.Position.Longitude);

                showUserLocation(e.Position.Coordinate.Point.Position.Latitude, e.Position.Coordinate.Point.Position.Longitude);
            };

            // Subscribe to StatusChanged event to get updates of location status changes.
            _geolocator.StatusChanged += (s, e) =>
            {
                System.Diagnostics.Debug.WriteLine("StatusChanged: " + e.Status);

                if (e.Status == PositionStatus.Ready)
                {
                }
            };
        }

        private void configureGeofenceMonitor()
        {
            _geofences = GeofenceMonitor.Current.Geofences;
            GeofenceMonitor.Current.GeofenceStateChanged += OnGeofenceStateChanged;
            GeofenceMonitor.Current.StatusChanged += OnGeofenceStatusChanged;
            createGeofence();
        }

        private void OnGeofenceStatusChanged(GeofenceMonitor sender, object args)
        {
            System.Diagnostics.Debug.WriteLine(sender.Status + "");
        }

        private async void OnGeofenceStateChanged(GeofenceMonitor sender, object args)
        {
            var reports = sender.ReadReports();
            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                {
                    foreach (GeofenceStateChangeReport report in reports)
                    {
                        GeofenceState state = report.NewState;

                        Geofence geofence = report.Geofence;

                        if (state == GeofenceState.Removed)
                        {
                            // Remove the geofence from the geofences collection.
                            GeofenceMonitor.Current.Geofences.Remove(geofence);
                        }
                        else if (state == GeofenceState.Entered)
                        {
                            // Your app takes action based on the entered event.

                            // NOTE: You might want to write your app to take a particular
                            // action based on whether the app has internet connectivity.
                            System.Diagnostics.Debug.WriteLine("You have entered geofence!");
                            showNotificationPin(report.Geoposition.Coordinate.Point.Position.Latitude, report.Geoposition.Coordinate.Point.Position.Latitude);
                        }
                        else if (state == GeofenceState.Exited)
                        {
                            // Your app takes action based on the exited event.
                            // NOTE: You might want to write your app to take a particular
                            // action based on whether the app has internet connectivity.
                            System.Diagnostics.Debug.WriteLine("You have exited geofence!");
                            if(_dialogsList.Count>0)
                            _dialogsList[0].Hide();
                        }
                    }
                });
        }

        private void createGeofence()
        {
            ExtendedGeofenceFactory extendedGeofence = new ExtendedGeofenceFactory();
            Geofence createdGeofence = extendedGeofence.CreateGeofence("FENCE", <LATITUDE HERE>, <LONGITUDE HERE>, 0, 40, false, 2, 1);
            if (_geofences != null)
                if (!_geofences.Any(geofence => geofence.Id.Equals(createdGeofence.Id)))
                    _geofences.Add(createdGeofence);
        }

        private async void showUserLocation(double latitude, double longitude)
        {
            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                MessageMapControl.MapElements.Remove(_userLocationIcon);
                _userLocationIcon = new MapIcon();
                _userLocationIcon.Image = RandomAccessStreamReference.CreateFromUri(new Uri("ms-appx:///Assets/LocationPin.png"));
                Geopoint userLocationGeoPoint = new Geopoint(new BasicGeoposition()
                {
                    Latitude = latitude,
                    Longitude = longitude,
                    Altitude = 0
                });

                _userLocationIcon.Location = userLocationGeoPoint;
                _userLocationIcon.NormalizedAnchorPoint = new Point(0, 0.5);
                _userLocationIcon.Title = "I am here";
                MessageMapControl.Center = userLocationGeoPoint;
                MapControl.SetLocation(_userLocationIcon, userLocationGeoPoint);
                MessageMapControl.MapElements.Add(_userLocationIcon);
                System.Diagnostics.Debug.WriteLine("Map elements count: " + MessageMapControl.MapElements.Count);
            });
        }

        private async void showNotificationPin(double latitude, double longitude)
        {
            PlaceContentDialog newPersonDialog = new PlaceContentDialog("PLACE TITLE", "This is sample text.");
            _dialogsList.Add(newPersonDialog);
            await newPersonDialog.ShowAsync();
        }
    }

 

Launch the app and check if geofence work but before that remember to enable “Location” capabilitiy in the manifest file:

Geo2

Sum up

Now you can start creating great apps with geofencing. Please also note that my example is available on Github here. I have also added background task to detect user location changes even if application is suspended.

Have fun!

Advertisements