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”.
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.
The user passes Sigma(in Malmö).
The user is now close to Pentia(in Copenhagen).
The user is near Magnetix(in Copenhagen).
The user passes Codehouse(in Copenhagen)
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.
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
The geo targets are the Sitecore partners which is listed in the “Client tracker” module.
The rule will also need a radius range.
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.
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 🙂
Awesome idea!
LikeLiked by 1 person
Thank you 🙂
LikeLike
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.
LikeLiked by 1 person
Thank you 🙂 Ah yes that is a very good idea, I never thought about that. Hmm, I will try to do that and come back to you
LikeLike