Transportation management (TMS) mileage engine based on Bing Maps

Transportation management module (TMS) includes a number of extension points that allow for custom algorithms for tasks such as calculations for rates and transit time or mileage on a specific route. Dynamics AX 2013 R3 comes with a number of engines, including a point-to-point mileage engine that is defined as a Microsoft.Dynamics.Ax.Tms.Bll.P2PMileageEngine engine type. The point-to-point mileage engine requires that data is defined in AX in order to determine mileage between two addresses. This engine is good for smaller size distribution networks, but can become expensive in maintenance when working with a large number of destinations (for example, for use with an online retail store). For large distribution networks it makes sense to use external services, such as Bing Maps.

This blog post describes how to build a custom engine that can retrieve mileage data by using the Bing Maps SOAP services. There are other services that can be used for this purpose, for example the Bing Maps REST services, but these are not described in this document.

The following article provides more information about the Bing Maps SOAP services:
http://msdn.microsoft.com/en-us/library/dd221354.aspx

The following article provides more information about the Bing Maps REST services:
http://msdn.microsoft.com/en-us/library/ff701713.aspx

For more information about how to build a custom TMS engine, read following whitepaper:
http://www.microsoft.com/en-us/download/details.aspx?id=43385

The zipped source code of the discussed C# code is available for download as this blog file attachment CustomTMSEngines.zip.

Prerequisites

Despite installing Visual Studio tools for AX, we will need to create access keys in order to consume Bing Maps services. For commercial purposes we would need to create an enterprise key, but for testing a free trial key is enough. You can create a new key on following portal:

https://www.bingmapsportal.com/

Note that this requires a valid Live ID account.

The new key will be displayed in your profile. The following illustration shows an example.

Implement a custom engine

To implement a custom engine, follow these steps:

1.       Open the following project in the AOT:

\Visual Studio Projects\C Sharp Projects\Microsoft.Dynamics.AX.Tms
in Visual Studio.

2.       Add a basic TMS engine implementation project to your solution (C# class library). Let’s call the project CustomTMSEngines. Add reference to Microsoft.Dynamics.AX.Tms and add the project to AOT. Configure it to be deployed on AOS.

For step-by-step instructions for creating such a project, see the whitepaper titled Implementing and Deploying Transportation Management Engines for Microsoft Dynamics AX 2012 R3. The paper describes how to build simple TMS engines.

3.       Add service references to Bing Maps geocode service and Bing Maps route service.

To do that, follow these steps:

a.       Select the project node in Visual Studio

b.      Context menu>Add Service Reference

c.       Put this URL to “Address” and select “Go”:
http://dev.virtualearth.net/webservices/v1/geocodeservice/geocodeservice.svc?wsdl

d.      Name it BingMapsGeocodeService

e.      Click OK

f.        Repeat steps A-E for the following WSDL, and name it BingMapsRouteService:
http://dev.virtualearth.net/webservices/v1/routeservice/routeservice.svc?wsdl

4.       Your Solution Explorer should now look like this:

 5.       Create a new class called BingMapsServicesClient.

The purpose of this class is to retrieve the distance from address A to address B by processing requests and responses from the Bing Maps service. The process consists of two steps:

a.       Retrieve geo-locations for address A and address B

b.      Retrieve distance from geo-location of A to B

For now, let’s just implement the constructor with one parameter “key” that will be part of the class state. The key parameter holds the value of the authentication key that we created earlier using our Live ID account.

///<summary>
/// Provides access to Bing Maps services.
///</summary>
internal class BingMapsServicesClient
{
    private string key;

    ///<summary>
    /// Creates a new instance of <c>BingMapsServicesClient</c> class.
///</summary>
///<param name=”key”>Bing Maps services access key.</param>
    public BingMapsServicesClient(string key)
    {
        this.key = key;
    }
}

 6.       The next step is to add a method to BindMapsServicesClient, which will allow us to retrieve geo-location, based on address string.

private BingMapsRouteService.Location GeolocationAddress(string address)
{
    BingMapsRouteService.Location result = null;
    GeocodeRequest geocodeRequest = new GeocodeRequest(); 

    // Set the credentials using a valid Bing Maps key
    geocodeRequest.Credentials = new BingMapsGeocodeService.Credentials();
    geocodeRequest.Credentials.ApplicationId = this.key; 

    // Set the full address query
    geocodeRequest.Query = address;
    // Set the options to only return high confidence results
    ConfidenceFilter[] filters = new ConfidenceFilter[1];
    filters[0] = new ConfidenceFilter();
    filters[0].MinimumConfidence = BingMapsGeocodeService.Confidence.High;

// Add the filters to the options
    GeocodeOptions geocodeOptions = new GeocodeOptions();
    geocodeOptions.Filters = filters;
    geocodeRequest.Options = geocodeOptions;

    // Make the geocode request
    Binding binding = new BasicHttpBinding();
    EndpointAddress endpointAddress = new EndpointAddress(@”http://dev.virtualearth.net/webservices/v1/geocodeservice/GeocodeService.svc”);

    GeocodeServiceClient geocodeService = new GeocodeServiceClient(binding, endpointAddress);
    GeocodeResponse geocodeResponse = geocodeService.Geocode(geocodeRequest); 

    if (geocodeResponse.Results.Length > 0)
    {
        result = new BingMapsRouteService.Location()
        {
            Latitude = geocodeResponse.Results[0].Locations[0].Latitude,
            Longitude = geocodeResponse.Results[0].Locations[0].Longitude
        };
    }

    return result;
}

There are a few interesting things in this method that are worth mentioning. First, the method signature contains only one string parameter for address, which is treated as query for the Bing Maps service. The format of this address must enable Bing Maps to identify the address. The best way to test whether your query fits this format is to test it through your web browser at http://www.bing.com/maps/.

Second, the endpoint address URI is hardcoded in the method. Typically endpoint and bindings are defined in the application configuration file. This may not be the most convenient solution for AX because configuration changes (for example, switching between production and test service URI) would require that the AOS is restarted. The best approach here would probably be to query the URI from AX database using AX Managed Interop. A convenient way to do this is to use the proxy classes for AX tables and LINQ to AX for building queries. The following article provides information about working with LINQ to AX: http://msdn.microsoft.com/en-us/library/jj677293.aspx.

If we decide to source the URI from AX, we need to keep security in mind and treat the URI as a protected resource and do the proper threat modeling.

7.       The next step is to add the method for retrieving mileage between two geographical locations. Add the following method to the BingMapsServicesClient class.

private double RetrieveDistance(
    BingMapsRouteService.Location locationFrom,
    BingMapsRouteService.Location locationTo)
{
    BingMapsRouteService.RouteRequest routeRequest = new BingMapsRouteService.RouteRequest();
    routeRequest.Credentials = new BingMapsRouteService.Credentials();
    routeRequest.Credentials.ApplicationId = this.key;
    routeRequest.Waypoints = new Waypoint[2]
    {
        new Waypoint() { Description = “Start”, Location = locationFrom },
        new Waypoint() { Description = “End”, Location = locationTo }
    };

    Binding binding = new BasicHttpBinding();
    EndpointAddress endpointAddress = new EndpointAddress(@”http://dev.virtualearth.net/webservices/v1/routeservice/routeservice.svc”); 

    RouteServiceClient routeService = new RouteServiceClient(binding, endpointAddress);
RouteResponse routeResponse = routeService.CalculateRoute(routeRequest);
    return routeResponse.Result.Summary.Distance / 1.609344;
}

 Again, this method uses a hardcoded URI for the route service, but it can be changed in a similar way as the GeolocationAddress method. Note that the distance that is retrieved from the Bing Maps service uses metric units. To convert kilometers to miles we divided it by the fixed factor.

 8.       To ensure that our code can compile we need proper imports. Add the following imports to BingMapsMileageEngine.cs:

    using System.ServiceModel;
    using System.ServiceModel.Channels;
    using CustomTMSEngines.BingMapsGeocodeService;
    using CustomTMSEngines.BingMapsRouteService;

 9.       The last part that is missing in the BingMapsServicesClient class is a public method that will allow us to call the retrieved distance between two addresses without having to couple the consumer with the Bing Maps service proxy object model. The following method does this.

///<summary>
/// Retrieves address between two fully specificed addresses.
///</summary>
///<param name=”addressFrom”>Origin address.</param>
///<param name=”addressTo”>Destination address/</param>

///<returns>Distance specified in miles.</returns>

public double RetrieveDistance(string addressFrom, string addressTo)

{

BingMapsRouteService.Location locationStart = this.GeolocationAddress(addressFrom);
BingMapsRouteService.Location locationEnd = this.GeolocationAddress(addressTo);

    return RetrieveDistance(locationStart, locationEnd);
}

 10.   The next part is to implement is the actual mileage TMS engine that will consume the functionality provided by the BingMapsServiceClient class. The following code block contains the full implementation of this class.

namespace CustomTMSEngines
{
    using System;
    using System.ServiceModel;
    using System.Xml.Linq;
    using Microsoft.Dynamics.Ax.Tms.Bll;
    using Microsoft.Dynamics.Ax.Tms.Data;
    using Microsoft.Dynamics.Ax.Tms.Utility;
    using Microsoft.Dynamics.Ax.Tms;

    ///<summary>
    /// The <c>BingMapsMileageEngine</c> class retrieves mileage data using Bing Maps services.
    ///</summary>
    class BingMapsMileageEngine : IMileageEngine
    {
        private string mileageEngineCode;

///<summary>
        /// Initializes the engine instance.
        ///</summary>
        ///<param name=”mileageEngine”></param>
        public void Initialize(TMSMileageEngine mileageEngine)
        {
            mileageEngineCode = mileageEngine.MileageEngineCode;
        }

        ///<summary>
        /// Retrieves mileage.
        ///</summary>
        ///<param name=”transactionFacade”>TMS transaction facade.</param>
        ///<param name=”currentElement”>XML element to for which to apply the mileage result.</param>
        ///<returns>The mileage response.</returns>
        public MileageHelperResponse RetrieveMiles(TransactionFacade transactionFacade, XElement currentElement)
        {
            MileageHelperResponse result = new MileageHelperResponse(this.mileageEngineCode);
            string key = YOUR BING MAPS ACCESS KEY IS ASSIGNED HERE;

            XElement pickUp = currentElement.GetAddress(AddTypeXmlConstants.Pickup);
            XElement dropOff = currentElement.GetAddress(AddTypeXmlConstants.DropOff);
            BingMapsServicesClient bingMapsServicesClient = new BingMapsServicesClient(key);

            try
            {
                string addressFrom = this.retrieveAddress(pickUp);
                string addressTo = this.retrieveAddress(dropOff);
            result.Miles = Convert.ToDecimal(bingMapsServicesClient.RetrieveDistance(addressFrom, addressTo));
            }
            catch (FaultException)
            {
                result.ErrorDto = new ErrorDto()
                {
                    Code = “100”,
                    Description = “Failure when retrieving mileage data from Bing Maps service.”
                };
            }
            catch (Exception e)
            {
                throw TMSException.Create(string.Format(“Unexpected failure happened when calling {0} mileage engine.”, mileageEngineCode), e);
            }
            return result;
        }

        private string retrieveAddress(XElement addressElement)
        {
            return formatAddress(
                addressElement.GetString(AddElementXmlConstants.Address),
                addressElement.GetString(AddElementXmlConstants.City),
                addressElement.GetString(AddElementXmlConstants.State),
                addressElement.GetString(AddElementXmlConstants.PostalCode),
                addressElement.GetString(AddElementXmlConstants.CountryRegion));
        }

        private string formatAddress(string address, string city, string stateProvince, string postalCode, string country)
        {
            string result = string.Format(“{0}, {1}, {2} {3}, {4}”, address, city, stateProvince, postalCode, country);

            return result;
        }
    }
}

To make this class work we must ensure that the correct Bing Maps service access key is assigned to the key variable in the RetrieveMiles method. For testing, we can hardcode the value, but this approach should not be used for the production environment. The access key should be treated as a protected resource and should be stored and sourced securely.

Enablement and testing

After deploying the solution to the server and restarting the AOS, the CustomTMSEngines is available under [server bin]\VSAssemblies. We can now create a mileage engine that uses our new engine code and enable it for rating. To do that, follow these steps:

1.       Click Transportation management > Setup > Engines > Mileage engines.

2.       Create a new engine using the following fields values:

a.       Mileage engine=BingMaps

b.      Name=Bing Maps Mileage Engine

c.       Engine assembly=CustomTMSEngines.dll

d.      CustomTMSEngines.BingMapsMileageEngine

We do not need to define engine metadata and data because our engine uses external sources.

The following screenshot shows the BingMaps engine with the settings we’ve just entered.

 We don’t have to define engine parameters because the current implementation does not require them.

Yes, the form should be empty, as shown in the screenshot above.

3.       Now it’s time to make this engine consumed by a rate engine that uses mileage when calculating rates. Click Transportation management > Setup > Engines > Rate engine.

4.       For this exercise we will modify a preconfigured rate engine so that it uses BingMaps mileage engine (we could also define a new rate engine based on the already defined engine type, associate it with our new mileage engine, and use them side-by-side). If we initialized our system using the Initialize base engine data button in Transportation management parameters, we find a record that uses following rate engine type:

Microsoft.Dynamics.Ax.Tms.Bll.MileageRateEngine.

Select the engine, and then click the Parameters button.

5.       We can now associate the MileageRateEngine with the BingMaps engine. To do this, set BingMaps as the value of the MileageEngineCode parameter, as shown in the following screenshot.

 

6.       Now let’s open the Rate route workbench and perform an ad-hoc rating, as shown in the following as on the screenshot.

7.       If we compare the results of the ad-hoc rating with the routing result from the Bing Maps web page, the result of 6.1 miles is the same as on the mileage response from TMS system.

Conclusion

It is fairly easy to build TMS engines that consume external services. This blog post has described a sample case in which we built a mileage engine that consumes mileage data from Bing Maps. Similar steps would be used to integrate with other mileage data providers. There are some products on the market that are specifically designed for transportation mileage calculation.

To wrap this up, I’d like to share a few tips with you if you decide to build your own engines that communicate with external systems:

·        Make sure to threat model your solutions, and ensure that service endpoint data and authentication data is secured.

·        Visual Studio unit tests are a good approach for testing the engine in isolation. If you go for X++ unit tests, restarting the AOS is required for every production code change.

·        The advantage of relying on external routing systems is that you don’t need to maintain address and routing data. Unfortunately, the disadvantage is that it can happen that some addresses recorded in AX may not be available in your external mileage system (for example, street numbers may be missing). You may consider writing a specific test or a job that iterates through all the transportation destinations and verifies that the mileage data can be retrieved. Otherwise you will discover those during operation.

CustomTMSEngines.zip