Lightweight Geospatial in Dynamics 365 for Phones


Bringing geospatial insights to business solutions is certainly nothing new; mapping and spatial searching are core components of the Dynamics 365 for Field Service, and many ISV solutions are available to glean location-based insights to optimize operations in Dynamics.

In this post, we will see how we can quickly and easily bring lightweight geospatial capabilities to the Dynamics 365 for Phones application, with nothing more than a Bing Maps key, and a Web Resource that we will create. This 60 second video (with voice-over) shows our end-result in action:

 

We will enable our users in the field to:

  • Quickly open account records based on proximity, saving time and increasing adoption
  • Visualize customer buildings and sites to streamline visits
  • Identify nearby accounts for ad-hoc visits, to optimize time in the field

We will create an HTML Web Resource that will identify the user’s location, search the Web API to find nearby accounts, then plot those accounts (with corresponding entity images) on a map. While in the field, users can launch their app, and immediately bring up account records as they approach.

 

Prerequisites

The prerequisites for building and deploying our account mapping in D365 for Phones include:

  • An instance of Dynamics 365 (Online)
    • You can request a trial of Dynamics 365 here
  • Geocoded Account Records, with address1_latitude and address1_longitude attributes populated (note that the Field Service module for Dynamics includes in-built geocoding capabilities)
  • The Dynamics 365 for Phones app installed on your mobile device
  • The text editor of your choice
  • A Bing Maps for Enterprise key
    • You can obtain a key using the instructions found here

 

Creating our HTML Web Resource

In a Web Resource, we can use JavaScript to not only access the Dynamics Client API, but also the Bing Maps v8 control for interactive mapping. As a result, most of our work will be done in JavaScript.

Using the text editor of our choice, we will begin building our web resource. We start with our HTML document, in which we include the JavaScript library for our Dynamics Client API, along with some global variables defined in JavaScript:

 

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8" />
    <script src="ClientGlobalContext.js.aspx" type="text/javascript"></script>
    <script type='text/javascript'>
        var layer;
        var userLoc;
        var map;
        // update this value to increase or decrease the distance
        //  around your current location to find nearby accounts:
        // alternately, you can update the code to use the current map view to trigger searches
        //   for accounts within the viewport as the user navigates
        var filterLatLonDifference = 0.05;
        // Your Bing Maps Key:
        //  Get one here: https://docs.microsoft.com/en-us/bingmaps/getting-started/bing-maps-dev-center-help/getting-a-bing-maps-key
        var bingMapsKey = 'YourBingMapsKey';
....

 

Creating our Map

We define our GetMap function (to be called when the Bing Maps JavaScript library has loaded) such that it will:

  • find the user’s location using the Xrm.Device.getCurrentPosition method
  • instantiate an interactive map in our HTML page, and
  • call the getNearbyAccounts function, passing the user's location
  •  

            function GetMap() {
                map = new Microsoft.Maps.Map('#myMap', {
                    credentials: bingMapsKey,
                    center: new Microsoft.Maps.Location(51.50632, -0.12714),
                    zoom: 2
                });
    
                if (Xrm.Page.context.client.getClient() == "Mobile") {
    
                    // Alert user that we are finding their location:
                    document.getElementById('message').style.display = "block";
                    // Find user's location:
                    Xrm.Device.getCurrentPosition().then(
                        function success(location) {
    
                            userLoc = new Microsoft.Maps.Location(
                                parseFloat(location.coords.latitude.toFixed(6)),
                                parseFloat(location.coords.longitude.toFixed(6)));
                            // Hardcode location here for testing purposes:
                            //userLoc = new Microsoft.Maps.Location(
                            //    47.647889, -122.140363);
    
                            //Add a pushpin at the user's location.
                            var pin = new Microsoft.Maps.Pushpin(userLoc, { title: "You" });
                            map.entities.push(pin);
    
                            // retrieve nearby accounts:
                            getNearbyAccounts(userLoc.latitude, userLoc.longitude);
    
                        },
                        function (error) {
                            document.getElementById('message').innerHTML = "Sorry, cannot get location";
                            document.getElementById('message').style.display = "block";
    
                        }
                    );
                } else {
                    // Show user error message:
                    document.getElementById('message').innerHTML = "Sorry, mobile usage only";
                    document.getElementById('message').style.display = "block";
    
                }
            }

     

    Finding Nearby Accounts

    Our getNearbyAccounts function will receive the user's location as parameters, and will then:

    • build and execute a retrieveMultipleRecords call against the D365 Web API using a bounding box around our user’s location
    • load the Microsoft.Maps.SpatialMath module to enable us to readily determine distances between locations
    • sort our locations by distance, using the compareDistance function
    • call our AddPushpinCanvas function for each account
    • set the map view to optimally display all accounts and the user’s location

     

            // function to retrieve accounts in proximity of our user location:
            function getNearbyAccounts(lat, lon) {
                // set up Web API filter and select, using a bounding box:
                var filter = "?$select=accountid,address1_latitude,address1_longitude,name,entityimage_url" +
                    "&$filter=address1_latitude gt " + (lat - filterLatLonDifference) +
                    " and address1_latitude lt " + (lat + filterLatLonDifference) +
                    " and address1_longitude gt " + (lon - filterLatLonDifference) +
                    " and address1_longitude lt " + (lon + filterLatLonDifference);
    
                // Call the Web API to retrive accounts:
                Xrm.WebApi.online.retrieveMultipleRecords("account", filter).then(
                    function success(result) {
                        if (result.entities.length > 0) {
    
                            // Load spatial math module to help calculating distances,
                            //  to help re-order and show nearest of many results:
                            Microsoft.Maps.loadModule("Microsoft.Maps.SpatialMath", function () {
    
                                // Sort accounts by distance:
                                result.entities.sort(compareDistance);
                                var nearbyAccounts = result.entities;
    
                                // capture all pushpin locations to set optimal map view:
                                var locs = [userLoc];
    
                                var i;
                                // loop through up to 5 accounts, and add pushpins on the map:
                                for (i = 0; i < nearbyAccounts.length && i < 5; i++) {
                                    AddPushpinCanvas(nearbyAccounts[i], function (pin) {
                                        map.entities.push(pin)
                                    });
                                    locs.push(new Microsoft.Maps.Location(nearbyAccounts[i].address1_latitude, nearbyAccounts[i].address1_longitude));
                                }
                                // set the map view:
                                var rect = Microsoft.Maps.LocationRect.fromLocations(locs);
                                map.setView({ bounds: rect, padding: 30 });
                            });
    
                        } else {
                            map.setView({ center: userLoc, zoom: 14 });
                        }
                        document.getElementById('message').style.display = "none";
                    },
                    function (error) {
                        console.log(error.message);
                        document.getElementById('message').innerHTML = "Sorry, an error has occurred retrieving nearby accounts";
                        document.getElementById('message').style.display = "block";
                    }
                );
            }
    
            // function to reorder array by distance from user location:
            function compareDistance(a, b) {
                a.distance = Microsoft.Maps.SpatialMath.getDistanceTo(new Microsoft.Maps.Location(a.address1_latitude, a.address1_longitude), userLoc, Microsoft.Maps.SpatialMath.DistanceUnits.Miles);
                b.distance = Microsoft.Maps.SpatialMath.getDistanceTo(new Microsoft.Maps.Location(b.address1_latitude, b.address1_longitude), userLoc, Microsoft.Maps.SpatialMath.DistanceUnits.Miles);
                return a.distance - b.distance;
            }

     

    Adding Pushpins with Account Images and Click Handlers

    Our AddPushpinCanvas function will accept the account and callback function as parameters, and will then:

    • define a function to call after our account entity image has been loaded, to:

      • create a Canvas element
      • add the entity image to the canvas, cropped to a circle
      • add a border to the canvas
      • create a Pushpin using the Canvas we created as the icon
      • add a click handler to the pushpin, to open the appropriate account record
      • call the specified callback function, which will then add the pushpin to the map
    • load the entity image for the account, subsequently triggering the onload function

     

            function AddPushpinCanvas(account, callback) {
                // retrieve attributes for pushpin:
                var imageurl = account.entityimage_url,
                    name = account.name,
                    latitude = account.address1_latitude,
                    longitude = account.address1_longitude,
                    guid = account.accountid;
    
                // If no image, add default image:
                if (imageurl == null) { imageurl = "/_imgs/contactphoto.png";}
    
                // Instantiate image:
                var img = new Image();
    
                // Create user location based on lat and lon:
                var location = new Microsoft.Maps.Location(
                    latitude, longitude);
    
                // create a callback function for once the user's image has loaded:
                img.onload = function () {
                    // Create canvas based on desired pushpin dimensions:
                    var canvas = document.createElement('canvas');
                    var cw = canvas.width = 40;
                    var ch = canvas.height = 40;
                    var borderWidth = 2;
                    var ctx = canvas.getContext('2d');
    
                    // Add the entity image to the canvas
                    ctx.drawImage(img, borderWidth, borderWidth, 36, 36);
                    ctx.globalCompositeOperation = 'destination-in';
                    ctx.beginPath();
                    ctx.arc(cw / 2, ch / 2, (ch - borderWidth) / 2, 0, Math.PI * 2);
                    ctx.closePath();
                    ctx.fill();
    
                    // Add a border around the image:
                    ctx.beginPath();
                    ctx.arc(cw / 2, ch / 2, (ch - (2 * borderWidth)) / 2, 0, Math.PI * 2);
                    ctx.closePath();
                    ctx.globalCompositeOperation = 'source-over';
                    ctx.strokeStyle = "gray";
                    ctx.lineWidth = borderWidth;
                    ctx.stroke();
    
                    // create our pushin:
                    var pin = new Microsoft.Maps.Pushpin(location, {
                        //Generate a base64 image URL from the canvas.
                        icon: canvas.toDataURL(),
                        anchor: new Microsoft.Maps.Point(20, 20),
                        title: name
                    });
    
                    // add an event handler to open the account form when clicked:
                    Microsoft.Maps.Events.addHandler(pin, 'click', function () {
                        Xrm.Utility.openEntityForm("account", guid);
                    });
    
                    // call the callback function:
                    if (callback) {
                        callback(pin);
                    }
                };
    
                // Load the image, which will initiate onload handler after loaded:
                img.src = imageurl;
    
            }

     

    Our HTML Web Resource Body

    The body of our HTML document is minimal:

    • a DIV to present messages to the user as an overlay
    • a DIV to host our interactive map
    • a SCRIPT tag to load the Bing Maps v8 map control, with our GetMap function specified as the function to call upon load

     

    ...
        </script>
        <style>
            #message {
                display: none;
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                font-size: 30px;
                z-index: 100000;
                margin-left: 25%;
                margin-right: 25%;
                position: absolute;
                width: 50%;
                height: 30%;
                border: 4px solid grey;
                background-color: white;
                color: black;
                text-align: center;
                vertical-align: middle;
                margin-top: 25%;
                padding-top: 30px;
                padding-bottom: 30px;
                padding-left: 10px;
                padding-right: 10px;
            }
            #map {
                position: relative;
                width: 100%;
                height: 100%;
            }
        </style>
    </head>
    <body style="margin:0px">
        <div id="message">Finding your location...</div>
        <div id="myMap"></div>
        <script type='text/javascript'
                src='https://www.bing.com/api/maps/mapcontrol?callback=GetMap'
                async defer></script>
    </body>
    </html>

     

    Creating a Dashboard

    We will now create a basic dashboard containing our web resource. First, we will upload our HTML document as a new web resource, navigating in the Web Client to Settings > Customizations > Customize the System > Web Resources > New:

     

     

    Note: Ensure you select Enable for Mobile.

    We then navigate to Settings > Customizations > Customize the System > Dashboards > New, and create a new Dashboard. We can choose any layout, but we then remove all but one component, which we populate with our web resource. Ensure you Enable Mobile for the Dashboard:

     

     

    And Enable Mobile for the Web Resource in the dashboard:

     

     

    In the App Designer for your desired Hub App, ensure your newly created dashboard has been added, then save and publish:

     

     

    Now, on your mobile device, launch the Dynamics 365 for Phones app, and navigate to Settings > Mobile Settings, ensuring that you have enabled User Content and Location:

     

     

    Now, if you navigate to the newly created dashboard in the app, you should be able to see your location, and any accounts in the vicinity:

     

     

    With this simple web resource, we can increase adoption, and empower our field teams with relevant geo-centric efficiency.

    The full Web Resource code for this sample can be found here.

Comments (0)

Skip to main content