Geofencing with real-time Geodata in Sitecore DMS

devices1

More and more users/visitors are using handheld devices with GPS for browsing the internet. That means we can get the their actual location thanks to the HTML5 Geolocation API.

After my previous posts, Track a visitors coordinates in the Sitecore DMS, I began to see the possibilities with “real-time geodata”.

There are some more goodies in the HTML5 Geolocation API we can use – Automatic tracking, speed and heading.

1. Automatic tracking allows you to track a users location while the website/webpage is open in the browser.

2. The speed feature gives you the users current speed.
That means we know if a user is:
Standing/sitting still
Walking
Driving/sitting a in vehicle

3. The heading feature gives you the current direction the user is moving in.

I’m going to present these topics in a couple of posts.

Today I will talk about Geofencing and how it can be implemented.
So what is geofencing?

Geo-fence is a virtual perimeter for a real-world geographic area. That means when someone using a device with GPS and crosses a “geofence” we will know. This is nothing new, geofencing has been used for a while in native apps.

Why not use it in a website?
In this case it’s a banner which will change content when a user/visitor is crossing a “geofence”. I was thinking of Sitecore and their partners – A partner banner 🙂
When a user is visiting the website, a banner will show the nearest partner from the users current location.

By default the banner presents the Sitecore HQ, the banner is using google maps “Streetview”.
Sitecore

When “Streetview” is closed a map is presented with driving/walking directions.
The speed feature in the HTML5 Geolocation API will determine if the user is driving or walking.
SitecoreMap

The user passes Sigma(in Malmö).
Sigma

SigmaMap

The user is now close to Pentia(in Copenhagen).
Pentia

PentiaMap

The user is near Magnetix(in Copenhagen).
Magnetix

MagnetixMap

The user passes Codehouse(in Copenhagen)
codehouse

codehouseMap

So how is it done?

First we need to get the Geo real time data.
We are doing it with javascript. Here we are using “automatic tracking” which will retrace each time the user has a new position.
The track type is defined in a “Client tracker” module.
trackingtypes

Tracktype

Init: function (trackType) {
  if (trackType == Sandbox.ClientTracker.TrackTypes.TrackPerRequest) {
    navigator.geolocation.getCurrentPosition(geoSuccess, geoError);
  }
  if (trackType == Sandbox.ClientTracker.TrackTypes.TrackFrequent) {
    // see https://developer.mozilla.org/en-US/docs/Web/API/Geolocation.watchPosition
    var watchID = navigator.geolocation.watchPosition(geoSuccess, geoError, Sandbox.ClientTracker.TrackFrequenzyOptions);
  }
  function geoSuccess(p) {
    window.coordinates = p.coords;
    Sandbox.ClientTracker.TrackUserLocation();
  }
},
TrackUserLocation: function () {
  var requestParamAndValues = {};
  requestParamAndValues["Coordinates"] = Sandbox.ClientTracker.StringFormat("{0},{1}", window.coordinates.latitude, window.coordinates.longitude, '1');

  if (window.coordinates.speed!=null)
    requestParamAndValues["GeoSpeed"] = window.coordinates.speed;

  if (window.coordinates.heading != null)
    requestParamAndValues["GeoHeading"] = window.coordinates.heading;

  var jsonObject = {};
  jsonObject["requestParamAndValues"] = requestParamAndValues;

  var analyticsEvent = new AnalyticsPageEvent(jsonObject, window.pathToHandler);
  analyticsEvent.trigger();
},
TrackTypes: {
   "NoTracking": 1,
   "TrackPerRequest": 2,
   "FrequentTracking": 3
},
TrackFrequenzyOptions: { //  see https://developer.mozilla.org/en-US/docs/Web/API/PositionOptions
   enableHighAccuracy: true,
   timeout: 10000, // Every 10 second 
   maximumAge: 0 //No caching
}

Next thing to do is to store the data. I’m very fond off the Visitor Tags in Sitecore DMS so that is where I will put it. I will use the TrackerHandler.ashx from previous post, Client Tracker with Sitecore DMS.

public class TrackerHandler : IHttpHandler, IRequiresSessionState
{

    public void ProcessRequest(HttpContext context)
    {
        context.Response.ContentType = Constants.ResponseContentTypes.ApplicationJavascript;
        Execute(context);
    }

    private static void Execute(HttpContext context)
    {

        if (!AnalyticsSettings.Enabled)
            return;

        string jsonData = context.Request[Constants.QueryParameters.JsonData];

        InputData inputData = JsonConvert.DeserializeObject(jsonData);

        if (inputData == null)
            return;

        if (!Tracker.IsActive)
            Tracker.StartTracking();

        Tracker.CurrentPage.Cancel();

        if (inputData.ContainsParamkey(InputDataKeys.Coordinates))
            TrackerService.AddTagToCurrentVisitor(InputDataKeys.Coordinates.ToString(), inputData.GetValueByKey(InputDataKeys.Coordinates));
                
        if (inputData.ContainsParamkey(InputDataKeys.GeoSpeed))
            TrackerService.AddTagToCurrentVisitor(InputDataKeys.GeoSpeed.ToString(), inputData.GetValueByKey(InputDataKeys.GeoSpeed));

        if (inputData.ContainsParamkey(InputDataKeys.GeoHeading))
            TrackerService.AddTagToCurrentVisitor(InputDataKeys.GeoHeading.ToString(), inputData.GetValueByKey(InputDataKeys.GeoHeading));

        if (inputData.ContainsParamkey(InputDataKeys.PageUrl) && inputData.ContainsParamkey(InputDataKeys.PageEventId))
            TrackerService.RegisterEventToAPage(inputData.GetValueByKey(InputDataKeys.PageEventId), inputData.GetValueByKey(InputDataKeys.PageUrl));

        Tracker.Submit();

    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

In order for the banners to present content per geolocation we need a rule that checks if a user is in range of a geo target
UsingTheRule

The geo targets are the Sitecore partners which is listed in the “Client tracker” module.
TargetLocations

TargetLocationItem

The rule will also need a radius range.
Locationranges

The rule defined in Sitecore
RuleSItecore

The actual code for the rule

public class VisitorCoordinatesWithinRadius<T> : WhenCondition<T> where T : RuleContext
{

        public string RadiusRangeId { get; set; }
        public string TargetLocationId { get; set; }

        protected override bool Execute(T ruleContext)
        {

            Assert.ArgumentNotNull((object)ruleContext, "ruleContext");

            Visitor visitor = Tracker.Visitor;

            VisitorLoadOptions visitorLoadOptions = new VisitorLoadOptions()
            {
                Options = VisitorOptions.VisitorTags
            };

            visitor.Load(visitorLoadOptions);

            GeoCoordinate? geoCoordinate = SetGeoCoordinates(Tracker.Visitor);

            if (!geoCoordinate.HasValue)
                return false;

            if (string.IsNullOrWhiteSpace(TargetLocationId))
                return false;

            if (string.IsNullOrWhiteSpace(RadiusRangeId))
                return false;

            return TargetLocationService.IsCoordinatesWithinRadiusRange(RadiusRangeId, TargetLocationId,
                geoCoordinate.Value.Latitude, geoCoordinate.Value.Longitude);

    }

    private GeoCoordinate? SetGeoCoordinates(Visitor visitor)
    {

        VisitorDataSet.VisitorTagsRow coordinatesTagsRow = visitor.Tags.Find(InputDataKeys.Coordinates.ToString());

        if (coordinatesTagsRow == null)
            return null;

        if (string.IsNullOrWhiteSpace(coordinatesTagsRow.TagValue))
            return null;

        return GeoCoordinateRepository.Get(coordinatesTagsRow.TagValue);
    }
}

TargetLocationService who checks if a user is within range

public class TargetLocationService
{
       public static bool IsCoordinatesWithinRadiusRange(string radiusRangeId, string targetLocationId, double latitudeToCompare, double longitudeToCompare)
       {
            GeoCoordinate? geoCoordinatesValueToCompare = GeoCoordinateRepository.Get(latitudeToCompare, longitudeToCompare);

            if (!geoCoordinatesValueToCompare.HasValue)
                return false;

            TargetLocation targetLocation = TargetLocationRepository.Get(Sitecore.Context.Database.GetItem(new ID(targetLocationId)));

            if (targetLocation == null)
                return false;

            if (!targetLocation.Coordinates.HasValue)
                return false;

            LocationRange radiusRange = LocationRangeRepository.Get(Sitecore.Context.Database.GetItem(new ID(radiusRangeId)));

            if (!radiusRange.Range.HasValue)
                return false;

            GeoCoordinateService geoLocationService = new GeoCoordinateService();

            double distanceInMeterToTarget = geoLocationService.DistanceToCoordinates(geoCoordinatesValueToCompare.Value,
                targetLocation.Coordinates.Value.Latitude, targetLocation.Coordinates.Value.Longitude,
                DistanceType.Meters);


            return (distanceInMeterToTarget <= decimal.ToDouble(radiusRange.Range.Value));


        }
}

The GeoCoordinateService that calculates the distance

public class GeoCoordinateService
{
    public double DistanceToCoordinates(GeoCoordinate coordinateToCheck, double endPointLatitude, double endPointLongitude, DistanceType distanceType)
    {
        double r = Sandbox.SharedSource.ClientTracker.Constants.GeoData.EarthRadiusInMeters;

        if (distanceType == DistanceType.Kilometers)
        r = Sandbox.SharedSource.ClientTracker.Constants.GeoData.EarthRadiusInKilometers;

        if (distanceType == DistanceType.Miles)
        r = Sandbox.SharedSource.ClientTracker.Constants.GeoData.EarthRadiusInMiles;

        double dLat = DegreeToRadian(endPointLatitude) - DegreeToRadian(coordinateToCheck.Latitude);
        double dLon = DegreeToRadian(endPointLongitude) - DegreeToRadian(coordinateToCheck.Longitude);

        double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + Math.Cos(DegreeToRadian(coordinateToCheck.Latitude)) * Math.Cos(DegreeToRadian(endPointLatitude)) * Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
        double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
        double distance = c * r;

        return Math.Round(distance, 2);
    }
}

Finally the google map javascript for presenting streetview, map and directions.
For directions we are using the users current “geo speed” in order to give walking or driving directions.
SpeedTypes

SpeedType

CreateMap: function () {
        var directionsService = new google.maps.DirectionsService();
        var directionsDisplay = new google.maps.DirectionsRenderer();
            var createMap = function (start) {

                var coordinatesArray = jQuery('#map').data('coordinates').split(',');

                var speedType = jQuery('#map').data('speedtype');

                if (speedType == null)
                    speedType = google.maps.DirectionsTravelMode.DRIVING;

                var targetDestination = new google.maps.LatLng(coordinatesArray[0], coordinatesArray[1]);
               
                var travel = {
                        origin : (start.coords)? new google.maps.LatLng(start.lat, start.lng) : start.address,
                        destination: targetDestination,
                        travelMode: speedType
                    },
                    mapOptions = {
                        zoom: 10,
                        // Default view: Malmö
                        center: new google.maps.LatLng(55.615056, 12.990449),
                        mapTypeId: google.maps.MapTypeId.ROADMAP
                    };

                map = new google.maps.Map(document.getElementById("map"), mapOptions);
                directionsDisplay.setMap(map);
                directionsDisplay.setPanel(document.getElementById("map-directions"));
                directionsService.route(travel, function(result, status) {
                    if (status === google.maps.DirectionsStatus.OK) {
                        directionsDisplay.setDirections(result);
                    }
                });
                
                var panoOptions = {
                    position: targetDestination,
                    addressControl: false,
                    addressControlOptions: {
                        position: google.maps.ControlPosition.TOP_LEFT
                    },
                    pov: {
                        heading: 100,
                        pitch: 5
                    },
                    linksControl: false,
                    panControl: false,
                    zoomControlOptions: {
                        style: google.maps.ZoomControlStyle.SMALL
                    },
                    enableCloseButton: true,
                    zoom: 0.4
                };

                var myPano = new google.maps.StreetViewPanorama(
                    document.getElementById('map'),
                    panoOptions);

                myPano.setVisible(true);
            };

        // Check for geolocation support	
        if (navigator.geolocation) {
           
            navigator.geolocation.getCurrentPosition(function (position) {
               
                // Success!
                createMap({
                    coords : true,
                    lat : position.coords.latitude,
                    lng : position.coords.longitude
                });
            }, 
                function () {
                    // Gelocation fallback: Defaults to Malmö, Sweden
                    createMap({
                        coords : false,
                        address: "Malmö, Sweden"
                    });
                }
            );
        }
        else {
            // No geolocation fallback: Defaults to Malmö, Sweden
            createMap({
                coords : false,
                address : "Malmö, Sweden"
            });
        }
    }

That’s it!

I think Geofencing is pretty cool. In this case I’m presenting a map and the directions to a location. But you can do so much more…
For instance if a visitor is passing a cafe, restaurant, or a shop – the website can push real time content. We have the users location and we will know if the user is walking or driving. We also know the date and the time. We can even find out what kind of weather it is…

You are only limited by your own imagination. Let it fly.

That’s all for now folks 🙂


7 thoughts on “Geofencing with real-time Geodata in Sitecore DMS

  1. Awesome, simply put.

    I think it would be fantastic if your API could request a child of a node automatically by using the geo fencing features. Something like Item GetNearestItem(CurrentposX, CurrentposY, Distance, ParentItem) or the like.

    Liked by 1 person

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.