With the Polaris release of Dynamics CRM Online, Bing Maps has been integrated directly into the Process forms for Accounts, Contacts and Leads. The integration is quite basic, however; the existing address details associated with the entity are geocoded when the form is displayed, and the results are used to show a map of the location. No additional data can be overlaid in the map, feedback on the quality of the geocode match are not provided, nor are the geocoded coordinates able to be refined and saved.

In this post, I will demonstrate how we can extend beyond the basic ‘out of the box’ solution using Bing Maps, and also extend it to on-premises CRM deployments as well as online. We will add additional geo-location values to CRM, using the Bing Maps for Enterprise developer APIs to enhance our ability to capture address data quickly and accurately, visualize it on maps, manually adjust latitudes and longitudes with Bing Maps imagery, and use reverse geocoding to find approximate nearby addresses to a given point. This post will provide an additional Bing Maps for Enterprise + Dynamics CRM integration scenario beyond the two previous posts: Heat Maps with Bing Maps and Dynamics CRM and Geocoding Dynamics CRM Data with Bing Maps.

By taking advantage of the address parsing and geocoding capabilities of Bing Maps, we enable CRM users to quickly enter partial or unformatted address data as a single string, and to have Bing Maps parse the address into individual formatted components; and, provide valuable visual and text-based feedback on the location and accuracy of the geocoding match for entities such as Accounts, Contacts and Leads.

For example, if a customer service representative is receiving address information being spoken to them by phone, they can take partial address information such as ‘1 microsoft way, redmond’, and populate their Account entity form with parsed address details:

AddressLocationCaptureAccountInfo

CRM users can also leverage the rich Bing Maps imagery and reverse geocoding capabilities to drag pushpins to specific locations on maps and imagery to capture their latitude and longitude, and provide approximate nearby address-based contextual information for entities such as Cases. For example, a municipality can use Bing Maps imagery to derive latitude and longitude information, and the approximate nearby address information for reports such as graffiti or potholes in 311 scenarios:

AddressLocationCaptureAccountInfo2

We will use Dynamics CRM Online, Bing Maps AJAX v7 control, and the Bing Maps REST Locations API in this post. Our final result will be a web resource that can be embedded in either a ‘Classic’ or ‘Updated’ entity form:

AddressLocationCaptureAccountInfo3

Full code for the web resource, usable in both ‘Classic’ and ‘Updated’ forms, can be found here.

Creating our Address and Location Capture Web Resource

We will present our location capture utility in a Dynamics CRM Online entity form, through the use of an HTML Web Resource.

When creating our HTML Web Resource, we must use the appropriate DOCTYPE declaration, add a META element with the charset attribute set to "utf-8", and include the Bing Maps AJAX v7 map control (to help us work with ‘Updated’ forms, we will also include jQuery).

The HTML for our page will be minimal. The map will occupy all available area, and will include a controls panel which will allow us to enter addresses to geocode, or coordinates to view. When the body has loaded, we will call our GetMap function, to instantiate our map:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html> 
 <head> 
   <title>Address and Location  Utility</title> 
   <meta http-equiv="Content-Type" content="text/html;  charset=utf-8"/>

<script type="text/javascript" 
src="https://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0&amp;s=1"></script>
<!-- We include JQuery from the  Microsoft Ajax CDN to assist with our 'Updated' forms attribute values access  --> 
<script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.2.min.js"></script> 

   <body onload="GetMap();"> 
     <div id='mapDiv' style="position:absolute; width:100%; height:100%;"></div> 
     <div id="controls"> 
     <div class="input"><h2>Geocode Tools</h2></div> 
     <div class="input"><b>Geocode Address:</b></div> 
     <div class="input"><input id="txtWhere" type="text" size="20" /></div> 
     <div class="input"><input id="GeocodeButton" type="button" value="Go" 
onclick="geocodeAddress()" class="button" /></div> 
     <div class="input"><b>Show location:</b></div> 
     <div class="input"><i>Latitude:</i></div> 
     <div class="input"><input id="txtLat" type="text" size="20" /></div> 
     <div class="input"><i>Longitude:</i></div> 
     <div class="input"><input id="txtLon" type="text" size="20" /></div> 
     <div class="input"><input id="LatLonButton" type="button" value="Go" 
onclick="showLatLon()" class="button" /></div> 
   </div>   
</body> </html>

In our GetMap function, we will instantiate our map, and extend the Microsoft.Maps.Pushpin class to allow us to associate geocoding metadata to pushpins we display. We also determine the form type, so we know whether we are using a Classic or Updated form. If using an Updated form, we will incorporate some alternate logic to update our entity attributes in the form. Note the placeholder for your Bing Maps Key. To obtain a Bing Maps Key, please see here.

// Initiate map:
function GetMap() {
    map = new Microsoft.Maps.Map(document.getElementById("mapDiv"), {
        credentials: "INSERT_KEY_HERE",
        zoom: 3, center: new Microsoft.Maps.Location(40.347, -94.218),
        mapTypeId: Microsoft.Maps.MapTypeId.birdseye
    });

    // extend pushpin class to hold geocoding data:
    Microsoft.Maps.Pushpin.prototype.geoname = null;
    Microsoft.Maps.Pushpin.prototype.geoentitytype = null;
    Microsoft.Maps.Pushpin.prototype.geoaddress = null;
    Microsoft.Maps.Pushpin.prototype.geoconfidence = null;
    Microsoft.Maps.Pushpin.prototype.geomatchcodes = null;
    Microsoft.Maps.Pushpin.prototype.geocalculationmethod = null;
    Microsoft.Maps.Pushpin.prototype.geopindragged = null;
    Microsoft.Maps.Pushpin.prototype.geoaddressrevgeo = null;

    // Get CRM Form Type, to choose behavior based on 
    //  Updated or Classic forms:
    crmFormType = parent.Xrm.Page.ui.getFormType();

}

When a user enters a text-based address or location and submits, we will call our geocodeAddress function, which will clear our map, extract the credential from our session, and use them to issue a request to the REST Locations API, Find a Location by Query. We specify geocodeServiceCallback as our callback function:

// clear map and retrieve credentials for REST Locations request:
function geocodeAddress() {
    map.entities.clear();
    map.getCredentials(callGeocodeService);
}

// call geocoding service with user-entered location details:
function callGeocodeService(credentials) {
    var searchRequest = 'https://dev.virtualearth.net/REST/v1/Locations/' +
        document.getElementById("txtWhere").value +
        '?output=json&jsonp=geocodeServiceCallback&key=' + credentials;
    var mapscript = document.createElement('script');
    mapscript.type = 'text/javascript';
    mapscript.src = searchRequest;
    document.getElementById('mapDiv').appendChild(mapscript);
}

In our callback function, we confirm that we have received geocoding results, and create a pushpin object for each result. We will also add the relevant geocoding metadata and formatted address details to each pushpin, so we will have it available for viewing and populating our form as needed. If we have a unique result, we will set the map view based on the bounding box returned with that geocoding result. If we have multiple results, we will auto-scale the map to show all results. Finally, we open an infobox showing the geocoding metadata for our top result:

// Callback for REST Locations request:
function geocodeServiceCallback(result) {

    if (result &&
    result.resourceSets &&
    result.resourceSets.length > 0 &&
    result.resourceSets[0].resources &&
    result.resourceSets[0].resources.length > 0) {
        var results = result.resourceSets[0].resources;
        var locationArray = new Array();
        for (var j = 0; j < results.length; j++) {

            var location = new Microsoft.Maps.Location(results[j].point.coordinates[0],
                results[j].point.coordinates[1]);
            var pushpin = createPushpin(location, (j + 1).toString());

            // get calculation method:
            var calculationMethod = getCalcMethod(results[j]);

            // Add geocoding metadata to pin:
            pushpin.geoname = results[j].name;
            pushpin.geoentitytype = results[j].entityType;
            pushpin.geoaddress = results[j].address;
            pushpin.geoconfidence = results[j].confidence;
            pushpin.geomatchcodes = results[j].matchCodes;
            pushpin.geocalculationmethod = calculationMethod;
            pushpin.geopindragged = false;
            pushpin.geoaddressrevgeo = false;

            // Add pin to map:
            map.entities.push(pushpin);

            // Add location to array for map auto-scaling:
            locationArray.push(location);
        }

        // Set view depending on whether result is unique or not:
        if (results.length == 1) {
            var bbox = results[0].bbox;
            var viewBoundaries = Microsoft.Maps.LocationRect.fromCorners(new Microsoft.Maps.Location(bbox[0], bbox[1]),
                new Microsoft.Maps.Location(bbox[2], bbox[3]));
            map.setView({ bounds: viewBoundaries });
        } else {
            // Show a best view for all locations
            var viewBoundaries = Microsoft.Maps.LocationRect.fromLocations(locationArray);
            map.setView({ bounds: viewBoundaries, padding: 75 });
        }
        // Open infobox for top result:
        showInfoBox(map.entities.get(0));
    }
    else {
        if (result && result.errorDetails) {
            alert("Message :" + response.errorDetails[0]);
        }
        alert("No results for the query");
    }
}

This allows users to manually enter a latitude and longitude to display a pushpin at that location. Ensure the latitude and longitude entered are valid, and then create a pushpin and display its infobox:

// Take user-entered coordinates, and add pushpin in that location:
function showLatLon() {

    // Clear existing entities:
    map.entities.clear();

    // get user input coordinates
    var lat = document.getElementById("txtLat").value;
    var lon = document.getElementById("txtLon").value;

    // Validate coordinates
    if (isLatLonValid(lat, lon) === false) {
        alert('Please enter valid WGS84 decimal values for Latitude and Longitude.');
        return false;
    }

    // Display location with pushpin:
    var latlonLocation = new Microsoft.Maps.Location(lat, lon);
    map.setView({ zoom: 12, center: latlonLocation });
    var pushpin = createPushpin(latlonLocation, "1");
    pushpin.geoname = "Manually Entered Location";

    // Add pushpin to map, and open infobox:
    map.entities.push(pushpin);
    showInfoBox(pushpin);

}

When we create any of our pushpins, we need to ensure they are draggable so the user can move the pushpin to refine the location manually. We also add event handlers which enable us to remind the user that the pin has been dragged, and to open the infobox when the pushpin is clicked:

// Create pushpin with appropriate  options and event handlers: 
function createPushpin(location, label) {
   var pushpin = new  Microsoft.Maps.Pushpin(location, { draggable: true, text:  label 
});
   var pushpinclick =  Microsoft.Maps.Events.addHandler(pushpin, 'click',  
pinClickHandler);
   var pushpindragend =  Microsoft.Maps.Events.addHandler(pushpin, 'drag',  
dragHandler);
   return pushpin;
} 

If users add a pushpin to the map based on a latitude and longitude, or drag any pushpin to a new location, we will give them the ability to capture approximate nearby address information for that location. Our callReverseGeocodeService function will receive the session credentials, and extract the current coordinates from the associated pushpin. These coordinates are used to issue a request to the REST Locations API, Find a Location by Point. We specify revgeoServiceCallback as our callback function:

// call reverse-geocoding service with current pushpin location:
function callReverseGeocodeService(credentials) {
    // Get pushpin location:
    var pinLocation = revgeoPushpin.getLocation();
    var searchRequest = 'https://dev.virtualearth.net/REST/v1/Locations/' + pinLocation.latitude + "," + pinLocation.longitude + '?output=json&jsonp=revgeoServiceCallback&key=' + credentials;
    var mapscript = document.createElement('script');
    mapscript.type = 'text/javascript';
    mapscript.src = searchRequest;
    document.getElementById('mapDiv').appendChild(mapscript);
}

In our callback function, we confirm that we have received reverse geocoding results, and add the resulting reverse-geocode metadata from the top result to the pushpin, so we will have it available for viewing and saving as needed. Finally, we open an infobox showing the new metadata for our location:

// Callback for REST Locations request:
function revgeoServiceCallback(result) {

    if (result &&
    result.resourceSets &&
    result.resourceSets.length > 0 &&
    result.resourceSets[0].resources &&
    result.resourceSets[0].resources.length > 0) {
        // Take only first result:
        var revgeoResult = result.resourceSets[0].resources[0];
        var location = new Microsoft.Maps.Location(revgeoResult.point.coordinates[0], revgeoResult.point.coordinates[1]);

        // get calculation method:
        var calculationMethod = getCalcMethod(revgeoResult);

        // Add geocoding metadata to appropriate pin:
        revgeoPushpin.geoname = revgeoResult.name;
        revgeoPushpin.geoentitytype = revgeoResult.entityType;
        revgeoPushpin.geoaddress = revgeoResult.address;
        revgeoPushpin.geoconfidence = revgeoResult.confidence;
        revgeoPushpin.geomatchcodes = revgeoResult.matchCodes;
        revgeoPushpin.geocalculationmethod = calculationMethod;
        revgeoPushpin.geoaddressrevgeo = true;


        // Open infobox for revgeo pin:
        showInfoBox(revgeoPushpin);
    }
    else {
        if (result && result.errorDetails) {
            alert("Message :" + response.errorDetails[0]);
        }
        alert("No results for the query");
    }
}

When we show our infoboxes, we retrieve the latitude, longitude, and geocoding metadata from the pushpin, and display this as our infobox content. The metadata we display includes:

  • The Display Name of the found location; e.g. ‘1 Microsoft Way, Redmond, WA 98052’
  • The Entity Type or classification of the found location; e.g. Address, Populated Place, etc.
  • The level of Confidence of our geocoding match; e.g. High, Medium or Low
  • The Match Codes indicating the geocoding level for the match; e.g. Good, Ambiguous, UpHierarchy

We use some logic to size our infobox based on data to be displayed, to optimize the available map area. And finally, we add some Actions to our infobox. Our Actions include:

  • Zoom: Zoom and center the map on this location
  • Reverse Geocode: If the pushpin has been added based on a lat/long, or if a pin has been dragged, find contextual address and location details
  • Populate Form: Add the address details and latitude and longitude of this location to our CRM entity

The Populate Form action is key, as it allows us to take our Bing Maps location data, and quickly add it to our CRM entity, without manually entering data in each form field. Note that within this action, we use alternate methodologies depending on the type of form being used: Classic or Updated. If using an Updated form, we use a workaround to populate the form values. We can revisit the need to do this after the Orion CRM release.

// Show infobox for a specific pushpin:
function showInfoBox(pushpin) {

    //Hide other infoboxes
    hideInfoBox();

    // Get pushpin location:
    var pinLocation = pushpin.getLocation();

    // Create the info box content for the pushpin
    var description = "<div class='pinContent'  style='border=1'>";
if (pushpin.geoentitytype != null) { description += "<div class='pinDetail'><b>Entity type:</b> " + pushpin.geoentitytype + "</div>" };
if (pushpin.geoconfidence != null) { description += "<div class='pinDetail'><b>Confidence:</b> " + pushpin.geoconfidence + "</div>" };
if (pushpin.geomatchcodes != null) { description += "<div class='pinDetail'><b>Match Codes:</b> " + pushpin.geomatchcodes + "</div>" }; if (pushpin.geocalculationmethod != null) { description += "<div class='pinDetail'><b>Calculation Method:</b> " + pushpin.geocalculationmethod + "</div>" }; description += "<div class='pinDetail'><b>Lat:</b> " + pinLocation.latitude + "</div>";
description += "<div class='pinDetail'><b>Lon:</b> " + pinLocation.longitude + "</div>";
if (pushpin.geoaddressrevgeo == true) { description += "<div class='pinDetail'><b style='color:red'>Alert: Address from RevGeo</b></div>" };
if (pushpin.geopindragged == true) { description += "<div class='pinDetail'><b style='color:red'>Alert: Pin Dragged</b></div>" };
description += "</div>"; // Determine infobox height based on specific pieces of content: var infoboxHeight = 180; infoboxHeight += (Math.max(0, (Math.ceil(pushpin.geoname.length / 25) - 1))) * 18; if (pushpin.geopindragged) infoboxHeight += 15; if (pushpin.geoaddressrevgeo) infoboxHeight += 15; infobox = new Microsoft.Maps.Infobox(pinLocation, { title: pushpin.geoname,
description: description, offset: new Microsoft.Maps.Point(7, 25), visible: true, zIndex:
1000, height: infoboxHeight }); infobox.setOptions({ actions: [ { label: "Zoom", eventHandler: function (mouseEvent) { // Zoom to pin location: map.setView({ zoom: 17, center: pinLocation }); } }, { label: "Reverse Geocode", eventHandler: function (mouseEvent) { // initiate reverse geocode: revgeoPushpin = pushpin; map.getCredentials(callReverseGeocodeService); } }, { label: "Populate Form", eventHandler: function (mouseEvent) { // Geocoding metadata will be available as properties of pushpin object // If using 'Classic Forms' (not the Read-Only forms of type 11): if (crmFormType != 11) { var controls = parent.Xrm.Page.ui.controls; controls.get("address1_latitude").getAttribute().setValue(pinLocation.latitude ?
pinLocation.latitude : "");
controls.get("address1_longitude").getAttribute().setValue(pinLocation.longitude ?
pinLocation.longitude : "");
controls.get("address1_line1").getAttribute().setValue(pushpin.geoaddress.addressLine ?
pushpin.geoaddress.addressLine : "");
controls.get("address1_city").getAttribute().setValue(pushpin.geoaddress.locality ?
pushpin.geoaddress.locality : "");
controls.get("address1_stateorprovince").getAttribute().setValue(pushpin.geoaddress.adminDistrict ?
pushpin.geoaddress.adminDistrict : "");
controls.get("address1_postalcode").getAttribute().setValue(pushpin.geoaddress.postalCode ?
pushpin.geoaddress.postalCode : "");
controls.get("address1_country").getAttribute().setValue(pushpin.geoaddress.countryRegion ?
pushpin.geoaddress.countryRegion : ""); } else { // Use workaround to populate address properties in Updated Process forms: // TO-DO: revisit after Orion release: prepAttribute('address1_line1'); prepAttribute('address1_city'); prepAttribute('address1_stateorprovince'); prepAttribute('address1_postalcode'); prepAttribute('address1_country'); prepAttribute('address1_latitude'); prepAttribute('address1_longitude'); setFormValue('address1_line1', (pushpin.geoaddress.addressLine ?
pushpin.geoaddress.addressLine : ""));
                    setFormValue('address1_city', (pushpin.geoaddress.locality ? 
pushpin.geoaddress.locality : ""));
                    setFormValue('address1_stateorprovince', (pushpin.geoaddress.adminDistrict ? 
pushpin.geoaddress.adminDistrict : ""));
                    setFormValue('address1_postalcode', (pushpin.geoaddress.postalCode ? 
pushpin.geoaddress.postalCode : ""));
                    setFormValue('address1_country', (pushpin.geoaddress.countryRegion ? 
pushpin.geoaddress.countryRegion : ""));
                    setFormValue('address1_latitude', (pinLocation.latitude ? 
pinLocation.latitude : ""));
                    setFormValue('address1_longitude', (pinLocation.longitude ? 
pinLocation.longitude : ""));

                }
            }
        }
        ]
    });

    map.entities.push(infobox);
}

*Important Notes on REST Locations API address data:

  • The primary purpose of the forward geocoder is to provide accurate latitude and longitude data for the given input information, and as a result the parsed address data returned in the response is subject to change in terms of exact format and content. Thus, it is important to ensure that you are not relying on specific or consistent formatting of address elements in responses
  • The reverse geocoder will provide an approximation of a nearby address to the given latitude and longitude provided as input, and as a result, your business processes assume that the address provided is precise, or definitively correct

You can download the complete Web Resource code using the link above.

Adding Web Resource to Form

We are now ready to add our Web Resource to the desired form in CRM. The first step is to upload the Web Resource as an HTML Web Resource using CRM’s Settings…Customizations…Web Resources tools. Next, use the Form Editor in the CRM Web Client to edit the form you want to add the address capture capability to. Make sure that the following attributes are visible on the form:

  • address1_line1
  • address1_city
  • address1_stateorprovince
  • address1_postalcode
  • address1_country
  • address1_latitude
  • address1_longitude

Now add your web resource to your form, and if you are planning to use an Updated form, ensure you tick the ‘Show this Web Resource in Read Optimized Form’:

AddressLocationCaptureWebResourceProperties

You are now ready to start capturing address and location details quickly and easily!

Conclusion

Being able to quickly and accurately capture address or location information in this manner has applicability in a number of CRM scenarios, such as:

  • Capturing new customer addresses provided by phone, as quickly and accurately as possible, without manually entering each address field. Even data that the customer may not provide, such as county information or zip code, can potentially be captured
  • Capturing accurate location information for Cases such as reports of potholes, graffiti, and more. CRM users can quickly orient the map to the general location by entering cross-streets, landmarks, or other location information, then drag the pin to capture latitude and longitude information using Bing Maps imagery, and additional information from the user

Bing Maps and Dynamics CRM can help your organization:

  • Spend less time capturing customer data
  • Ensure your addresses are captured correctly
  • Ensure you capture accurate coordinates for your entity data

Now that you have accurate location information for your CRM data, you can start using location-based trends to make smarter business decisions by adding heat maps to Dynamics CRM as well!

 

Geoff Innis
Bing Maps Technical Specialist