MapQuest Developer Blog

  • AOL Tour Tracker

    AOL Music just launched a great site called Tour Tracker. It allows you to see when and where your favorite bands are playing and shows coming to a town near you.

    They also just happen to have some really cool interactive maps built on the same MapQuest Platform that you use, specifically the JavaScript API. How about that?

    Go over and check it out while I get some tickets to go see the Foo Fighters.

  • New Feature: Printable Maps!

    You can now print MapQuest maps!

    "Well duh," you're probably thinking. "I've been printing MapQuest maps and driving directions for years."

    Well of course you have. MapQuest.com generates millions-and-millions of maps specifically for printing every day. What I'm talking about here is printing from the applications you're making using the Free Edition of the MapQuest Platform.

    "I couldn't do that before?"

    That was a little unclear in our old Terms and Conditions. Some observant developers asked about it, and we said "Why not?" We wound up drawing straws to see who had to ask Legal for the revisions. I lost. So I took the ask up the cliffside, knocked on the iron plated door, dropped the paperwork and ran.

    So here's the deal for printing with the Free Edition:

    • You can print up to 5000 copies of a map per run/publication
    • The printed map can be up to a maximum of 8.5" x 11" inches printed
    • It has to be a free publication (flyer, newsletter, etc); you can't use it in a publication you're charging for

    Just another example of how we're working with developers and responding to your needs.

    Happy Printing!

  • Beta Gone Bye-Bye

    With the 5.3 release of the MapQuest Platform, we've now deactivated the beta versions (until the next version is ready for beta of course). If you were using the 5.3 beta versions, here's some quick tips to keep your application running smoothly:

    Take the following <script> tag for example:
    <script src="http://btilelog.beta.mapquest.com/tilelog/transaction? transaction=script&key=<YOUR_KEY_HERE>&itk=true&v=5.3.0_RCx" type="text/javascript"></script>
    • You need to change "&v=5.3.0_RCx" to "&v=5.3.0," "x" being the Release Candidate number you were using
    • If you were pointing to the beta servers "http://btilelog.beta.mapquest.com," you need to point back to the production servers: "http://btilelog.access.mapquest.com"

    These simple steps will help keep your development from grinding to a halt -- like in a traffic jam. Since real-time traffic is also one of the new features in 5.3, you can help keep your users from actually winding up in a real traffic jam.

  • MapQuest Platform 5.3 Released

    We are pleased to announce the release of version 5.3 of the MapQuest Platform! This update focuses on enhancements to our 3 client-side APIs: JavaScript, AS3, and FUJAX.

    Some of the big changes include:

    • Collections: Support for multiple and remote collections (KML and GeoRSS); easier handling of shape collections
    • Custom Tile Layer support
    • Add real-time traffic to your map
    • Globe view enhancements
    • All 3rd party JavaScript libraries removed (decreases JS footprint)

    For the server-side APIs, .NET works with 2.0 and 3.5 on 32 and 64-bit; C++ now has been upgraded to support Visual Studio 2008 and 32 and 64-bit libraries are available.

    Here's the full list of release notes:

    New Features: General — All Languages

    • ShapeCollections To make it easier to handle multiple shape collections on the map, MQA.ShapeCollection and MQW.ShapeCollection (FUJAX) replaces both PoiCollection and OverlayCollection. POIs and overlays now go into the same collection. Adding, removing, replacing, and appending include new methods.
    • Custom Tile Layers Custom tile layers can now be added to maps.
    • Multiple Collections Multiple shape collections can now be added to the map. A collection can include POIs and Overlays. Once a collection is attached to the map, adding shapes to and/or removing shapes from the collection will be automatically reflected on the map. Decluttering can also be done by collection.
    • Remote Collections KML and GeoRSS support has been built into the Platform. When creating a RemoteCollection, all you need to do is tell it the location of the feed and its format, and the RemoteCollection will be displayed on the map.
    • Minimum and Maximum Zoom Levels on POIs Minimum and maximum zoom levels can be set on any POI. The POI would then be visible on the map only between the set zoom levels.
    • Retrieving an item by key A shape (POI or overlay) can now be retrieved from the map or any collection by its key.
    • Reference to parent collection A reference to the shape's (POI or overlay) parent collection can be returned.

    JavaScript and AS3 (ActionScript™ 3) Only

    • Traffic Flow, Incidents, and Market Data Traffic information can now be displayed on the map in the form of market data, incident data (construction, incidents, events), and a traffic flow overlay.

    AS3 and FUJAX Only

    • Globe View Globe view has been upgraded to include the following enhancements:
      • Spins on all axis
      • Setting the number of triangles to adjust for performance
      • Smooth zoom animations
      • Overlays & route highlights
      • Map rotation setting

    JavaScript and FUJAX Only

    • 3rd Party Libraries All 3rd party libraries have been removed from the API (Prototype and Dojo)

    JavaScript

    • MQA Namespacing The Tilemap toolkit portion of the API has been namespaced to MQA and all MQ leading characters have been removed. For example, it would now be MQA.Tilemap instead of MQTilemap.
    • MQMapIcon The MQMapIcon is now MQA.Icon and is purely an interface, taking the image URL and the setup values in the constructor. For example: myIcon = new MQA.Icon(imageURL, width, height, offsetX, offsetY);
    • POIs & Overlays Overhaul These are both now extended off of the same base object and their getters & setters have changed. Examples:
      myPoi.setValue(propertyNameAsString, valueObject);
      returnedValue = myPoi.getValue(NameAsString);
      • A .setValues(); method exists that takes literal notation to set as many properties at once as you want. Refer to the documentation for a complete list of property names and examples.
      • Existing getters and setters will still function, but are deprecated. Effort should be made to convert these to the new methods.
    • Drop Shadow A visual drop-shadow graphic has been added to the map and can be turned on by calling map.setMapShadowState(Boolean). The shadow is off by default.

    FUJAX

    • MQW Namespacing The FUJAX Tilemap toolkit portion of the API has been namespaced to MQW and all MQ leading characters have been removed. For example, it would now be: MQW.Tilemap instead of MQTilemap.

    Supported Environments

    JavaScript

    • Windows
      • Internet Explorer 6.0 and above
      • Mozilla Firefox 2.0 and above
    • Macintosh
      • Firefox 2.0 and above
      • Safari 3.0 and above

    Adobe® ActionScript™

    • Flash player: version 9,0,115 or greater
    • AS3 SWCs: all environments
    • Flex2 Components: Adobe® Flex™ Builder™ 2
    • CS3 Components: Adobe® Creative Suite® 3 (CS3) Flash

    Updates

    • .NET
      • The library has been renamed to mapquest20.dll to reflect that this is for .NET 2.0 and above only. It has also been tested with the 2.0 and 3.5 frameworks and in both 32-bit and 64-bit environments.
    • C++
      • Windows support has been upgraded to Visual Studio 2008
      • Both 32-bit and 64-bit versions of the libraries now exist
  • Creating a custom ASP.NET control for MapQuest

    In my last post we saw that using the MapQuest NET API version 5.3 in an ASP.NET web application was not all that much harder than using it in a winforms application. Now in order to easily reuse some of the code it would be nice to create an ASP.NET custom control which displays a map just based on an origin and a destination.

    So what to do?

    1. We want to put the control in a class library, so we add an item of type 'ASP.NET Server control named 'RouteMapControl'. This will create a class which inherits from WebControl.
    2. Next we add an Origin and Destination propert of type 'GeoAddress' (from the MQClientInterface namespace).
    3. Lastly we override the 'protected override void RenderContents( HtmlTextWriter output )' method and have it use the Origin and Address to create a route and use the route to create a map.

    This is what the code for the class looks like:

    using System;
    using System.ComponentModel;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    using MQClientInterface;
    using DevelopOne.MapQuest.Proxies;
    using DevelopOne.MapQuest.Extensions;
    
    namespace DevelopOne.MapQuest.Web.Controls
    {
        [DefaultProperty( "Text" )]
        [ToolboxData( "{0}:RouteMapControl>" )]
        public class RouteMapControl : WebControl
        {
            [Bindable( true )]
            [Category( "MapQuest" )]
            [DefaultValue( "" )]
            [Localizable( false )]
            public GeoAddress Origin
            {
                get
                {
                    GeoAddress o = new GeoAddress();
                    o.Init();
                    o.LatLng.Latitude = (double) ViewState["Origin_Latitude"];
                    o.LatLng.Longitude = (double) ViewState["Origin_Longitude"];
                    return o;
                }
    
                set
                {
                    ViewState["Origin_Longitude"] = value.LatLng.Longitude;
                    ViewState["Origin_Latitude"] = value.LatLng.Latitude;
                }
            }
    
            [Bindable( true )]
            [Category( "MapQuest" )]
            [DefaultValue( "" )]
            [Localizable( false )]
            public GeoAddress Destination
            {
                get
                {
                    GeoAddress o = new GeoAddress();
                    o.Init();
                    o.LatLng.Latitude = (double) ViewState["Destination_Latitude"];
                    o.LatLng.Longitude = (double) ViewState["Destination_Longitude"];
                    return o;
                }
    
                set
                {
                    ViewState["Destination_Longitude"] = value.LatLng.Longitude;
                    ViewState["Destination_Latitude"] = value.LatLng.Latitude;
                }
            }
    
            protected override void RenderContents( HtmlTextWriter output )
            {
                if ( this.Origin == null || this.Destination == null )
                {
                    return;
                }
                else
                {
                    if ( this.Width.IsEmpty )
                        this.Width = new Unit( 800 );
    
                    if ( this.Height.IsEmpty )
                        this.Height = new Unit( 600 );
    
                    RouteResults route;
                    using ( RouteServerProxy proxy = new RouteServerProxy() )
                    {
                        route = proxy.CreateRoute( this.Origin, this.Destination );
                    }
                    using ( MapServerProxy proxy = new MapServerProxy() )
                    {
                        string url = proxy.GetMapUrl( route.ShapePoints.ToLine(), (int)this.Width.Value, (int)this.Height.Value, true).ToString();
                        url = HttpUtility.HtmlEncode( url );
                        string html = "<img src=\"" + url + "\"/><br/>Distance: " + Math.Round(route.Distance, 2).ToString() + " miles.";
                        output.Write( html );
    
                    }
                }
            }
        }
    }
    

    Notice in the RenderContents method that:

    1. We're creating an HTML image tag, but also some text to display the distance of the route.
    2. The proxy.GetMapUrl has a boolean parameter as the fourth parameter. I discovered that for longer routes the amount of information in the Url becomes too much, making the Url too long and it won't load. Using the true parameter makes the proxy use a serverside session. Now the Url just contains a reference to the session and stays much shorter. The code needed to create a session is straightforward, use the Exec object in the MapQuest library.:
      string sessionId = Exec.CreateSessionEx( session );
      url = Exec.GetMapFromSessionURL( sessionId, new MQClientInterface.DisplayState() );

    By adding a reference to the class library Visual Studio 2008 will detect the control and make it available in the toolbar when you're editing an ASP.NET page. Just drag and drop the control on you form and your ready to go.

    I like my controls to have meaning full tags so I add a control reference in the web.config:

    
    <pages>
       <controls>
            <add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
            <add tagPrefix="asp" namespace="System.Web.UI.WebControls" assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
            <add tagPrefix="mq" namespace="DevelopOne.MapQuest.Web.Controls" assembly="DevelopOne.MapQuest"/>
      </controls>
    </pages>
    
    
    

    The markup in the page now looks like this:

    <mq:RouteMapControl ID="RouteMapControl1" runat="server" />
    

    If you choose to skip the web.config step you'll see the tag as 'cc1:RouteMapControl'.

    Happy coding!

    - Mark Blomsma

    Download the code here.

  • The AIM Map Phone

    Where Open Voice meets Geocoding

    Geocoding is hot. "Geocoding is the process of assigning geographic identifiers (e.g., codes or geographic coordinates expressed as latitude-longitude) to map features and other data records, such as street addresses. You can also geocode media, for example where a picture was taken, IP addresses, and anything that has a geographic component. With geographic coordinates the features can be mapped and entered into Geographic Information Systems" - wikipedia. Using the MapQuest API we can geocode any address in the world. In my recent article on Open Voice I showed how to use C# and .NET to build a Voice over IP phone. I've also been exploring the possibilities of the MapQuest .NET API on my blog. Now wouldn't it be great if those two worlds came together to build a phone that displays the location of the person you're calling? I call it the AIM Map Phone. And it looks like this:

    So what are the technologies used to build this phone? Well I found a commercial offering by Conaito which combines Voice over IP with and SIP programming stack: the VoIP SIP SDK. The SDK offers excellent .NET examples and implementing a basic VoIP phone took about an hour, not bad right? Having the phone working, the next step is to geocode the phone number. This turned out to be a two step process. ServiceObjects is a company which offers an XML web service for translating a phone number into an address. There are two separate services, DOTS GeoPhone which is for landlines and DOTS GeoPhone Wireless for wireless phones. The translated address can be used to find the geographical location using the MapQuest Geocode Server. The resulting coordinates can be used by the MapQuest Static Map Server to create a map of the location.
    Let's look at some of the code involved.

    Making the call

    In order to place a call using the AIM Call Out service we need to use the Open Voice API. The Open Voice API consists of an infrastructure which supports a number of open standards, allowing any standards compliant application or SDK to make use of the infrastructure. The Conaito VoIP SDK supports these standards and works like a charm with AIM Call Out. The client API is accessible from C# / .NET by dragging an ActiveX control onto the form. Calls are made by programming against this ActiveX control. In the AIM Map Phone the 'Dial' button works as a toggle and uses the following code:

      private bool _calling = false;
      private int _callId = -1;
      private void btnDial_Click( object sender, EventArgs e )
        {
        if ( _calling == true )
        {
        // hang up
        HangUp();
        }
        else
        {
        // make call
        DialNumber( txtNumber.Text );
            // ... update map ...
        }
        }
      private void DialNumber( string phoneNumber )
        {
        try
        {
        // check if previous call closed down correctly
        if ( _callId != -1 )
        {
        HangUp();
        }
        // First of all transport must be configured.
        this.axUserAgent.AddTransport( conaito.Transport.UDP, 5060 );
             // Find available RTP (media) port
        int port = this.axUserAgent.FindPort( 4000, 8000, 2, conaito.Transport.UDP
          );
             // Startup User Agent
        this.axUserAgent.Startup( port, 1,
        String.Empty,
        PhoneSetting.Default.STUNServer );
             // AIM account is used for SIP authentication
        string aimAccount = PhoneSetting.Default.AIMAccount;
        // name is used for registration
        string name = aimAccount.Substring( 0, aimAccount.IndexOf( "@" )
        );
            // REGISTER is not support by SIP.AOL.COM
        // but is required to initialize the axUserAgent
        this.axUserAgent.Registrator.AuthenticationId = aimAccount;
        this.axUserAgent.Registrator.Register(
        PhoneSetting.Default.SIPServer,
        name,
        PhoneSetting.Default.AIMDevicePassword,
        name );
            // Setup eventhandler for cleanup when call terminates
        this.axUserAgent.OnTerminated += new Axconaito._IUserAgentEvents_OnTerminatedEventHandler(
          axUserAgent_OnTerminated );
            // Make the call (asynchronous)
        // Number needs to be post fixed by "@sip.aol.com"
        _callId = this.axUserAgent.CallMaker.Invite(
        phoneNumber + "@" + PhoneSetting.Default.SIPServer );
           // Update UI
        _calling = true;
        this.btnDial.Text = "Hang up";
        }
        catch ( COMException exception )
        {
        ShowError( this.axUserAgent.LastError, exception.Message );
        }
        catch ( Exception exception )
        {
        ShowError( exception.Message, "General Exception" );
        }
        }
        private void HangUp()
        {
        // Hang up
        this.axUserAgent.CallMaker.Hangup( _callId );
        // Shutdown User Agent
        this.axUserAgent.Shutdown();
        _callId = -1;
        // Update UI
        _calling = false;
        this.btnDial.Text = "Dial";
        }
    void axUserAgent_OnTerminated( object sender, Axconaito._IUserAgentEvents_OnTerminatedEvent
        e )
        {
        HangUp();
        }
        

    The DialNumber methods follows the basic steps needed to start a call:

    1. Add a transport mechanism, in this case UDP.
    2. Prepare for RTP transport
      1. Find a port
      2. Startup the RTP host

        Note that the SDK allows for all sorts of custom tweaking of codecs, volume and input/output devices. This sample is using the default settings for all of those.
    3. Setup an eventhandler for when the call terminates.
    4. Send a SIP INVITE message to call the actual number.

      Note that the format of the phonenumber needs to be <phonenumber>@sip.aol.com.

    As you can see the sample retrieves configuration settings from the app.config file. These can be set using the Phone Settings configuration form:


    Geocode the number

    Using the ServiceObjects XML web service the phone number can be translated into an address. There are two API's one for landlines and one for wireless phones. It must be said that the service for landlines is excellent, but the one for wireless phone does not seem to know all the numbers, especially for where I live, numbers in Maine.

      private bool _calling = false;
      private int _callId = -1;
    private void btnDial_Click( object sender, EventArgs e )
        {
        if ( _calling == true )
        {
        // hang up
        HangUp();
        }
        else
        {
        // make call
        DialNumber( txtNumber.Text );
        GeoAddress address = GetAddress( txtNumber.Text );
        if ( address == null )
        {
        // unable to resolve phone number
        MessageBox.Show( this,
        "Phone number could not be translated to a geographical location.",
        "Information", MessageBoxButtons.OK, MessageBoxIcon.Information );
        }
        else
        {
        UpdateMap( address );
        }
        }
        }
    private GeoAddress GetAddress( string phoneNumber )
        {
        string americanPhoneNumber = phoneNumber.Replace( "+1", "" );
        PhoneInfo info;
        ServiceObjects.DOTSGeoPhone service = new DOTSGeoPhone();
        info = service.GetPhoneInfo( americanPhoneNumber, ServiceObjectSetting.Default.DOTSGeoPhoneLicenseKey
        );
        if ( info != null &&
        info.Contacts != null &&
        info.Contacts.Length > 0 )
        {
        Address address = new Address()
        {
        City = info.Contacts[0].City,
        Country = "US",
        PostalCode = info.Contacts[0].Zip,
        State = info.Contacts[0].State,
        Street = info.Contacts[0].Address
        };
            GeocodeServerProxy proxy = new GeocodeServerProxy();
        List<GeoAddress> result = proxy.Geocode( address );
        if ( result != null && result.Count > 0 )
        {
        return result[0];
        }
        return null;
        }
        else
        {
        return GetWirelessAddress( americanPhoneNumber );
        }
        }
    

    From a .NET perspective the service works really, really simple. Just add a web reference from your project (don't use VS 2008 Service Reference) to the DOTS GeoPhone service ( http://trial.serviceobjects.com/gp/GeoPhone.asmx?WSDL ), use the generated proxy and call the GetPhoneInfo service. Note: You will need to get a license key.

    The PhoneInfo object that is returned contains zero, one or more contacts. I the sample I take the first and use that to create a MapQuest .NET API Address object. This is passed to the GeocodeServerProxy, a custom proxy class I created (read this blog post for more details). This results in the GeoAddress of the phone number. Should the phone number not lead to a contact, then I try and use a similar method using the DOTS GeoPhone Wireless service.

    The GetAddress method uses the app.config to retrieve the license key for the ServiceObjects service. These can be set with the ServiceObjects Settings screen:


    Display the location

    The AIM Map Phone uses a custom Winforms control which will load and display a map based on the GeoAddress. The code is based on the MapQuest .NET 5.3 API, see this blog post on more information on building such a control. You will need a MapQuest account in order to use the service. The information can be set in the MapQuest Settings screen:


    The code

    Download the code for the sample here.
    In order to compile and run the code you will need to install VoIP SIP SDK v2.6 from Conaito. You will also need a (trial) account DOTS GeoPhone * DOTS GeoPhone Wireless from ServiceObjects.

    Happy coding!

  • 5.3rc5 Release for JavaScript and FUJAX APIs

    As we get closer to the final release of version 5.3 of the MapQuest Platform, we've just added Release Candidate 5 of our JavaScript and FUJAX APIs to the Beta page.

    The highlight in this release is the ability to define specific packages to be included in the code. With all of the new features we've been adding to the Platform, we recognized the need to optimize the size of your download with only the code necessary for your application.

    From the RC5 notes:

    If you want your application to have a traffic control, you can specify to include this package via the '&ipkg=controls1,traffic' URL name/value pair. However, if your application does not need the traffic control, simply use '&ipkg=controls1' and you'll save about 12k of download. This concept will be used going forward in order to optimize download sizes/speed.

    NOTE: We have separated out the map controls as a modular package - if you are using our default controls (Zoom Control, View Control, etc), you will want '&ipkg=controls1' on the string. If you are using fully customized map controls, you can now eliminate ours from the script download.

    The "ipkg" parameter

    controls1 - will bring down all 4 controls (largezoom, zoom, pan, and view).
    traffic - will bring down the traffic package. For example:

    • &ipkg=controls1 - will bring down just the controls package
    • &ipkg=traffic - will bring down just the traffic package
    • &ipkg=controls1,traffic - will bring down both the controls and traffic packages
    • &ipkg=controls1,traffic,package1,etc. - will bring down all packages listed by commas

    Also a reminder: If your a Free Edition developer, you will also need to sign-up for a Developer License for developing with MapQuest Platform Beta code.

  • Do You Shazou?

    Shazou (Japanese for "mapping") is a great Firefox plug-in developed by Seisan to map and geolocate any web site currently being viewed. It's another helpful tool against phishing scams.

    Shazou is powered by the MapQuest Platform: Free Edition AS3 API. Download it from the Shazou page on the Official Mozilla Firefox Add Ons site.

  • Using MapQuest 5.3 in an ASP.NET application

    Earlier this week I was thinking, wouldn't it be cool if the MapQuest .NET API would allow me to program all my logic in C# (or VB.NET) and then instead of having it generate a bitmap, have it generate just the URL of a map? The URL can be used in web applications or I can embed the url in an image tag which I send as an email. A whole new world of possibilities opens up. Guess what? Generating an URL for a MapQuest map is part of the API today!

    In a previous blog post I described how to use the Exec object to create a bitmap of a map:

    sbyte[] imageBytes = Exec.GetMapImageDirect( session );
    byte[] bytes = (byte[]) (Array) imageBytes;
    
    MemoryStream stream = new MemoryStream( bytes );
    Bitmap bitmap = new Bitmap( stream );
    

    Using the same Exec object, just use the GetMapDirectURLEx method to build an URL to the same image.

    string url = Exec.GetMapDirectURLEx( session, new MQClientInterface.DisplayState() );
    
    Just assign the URL to an the ImageURL property of an ASP.NET Image control and the map will be displayed.
    imgMap.ImageUrl = url;   // imgMap is an Image control
    

    I ran my code on the development environment and as such I get back a reference to the MapQuest dev box:

    http://map.dev.mapquest.com//mqserver.dll?e=0&GetMapDirect.1=Session:5,MapState:,,314159.265358,314159.265358,11.111111,8.333333,0,
    CoverageStyle:2,,DTStyle:3072,0,2147483647,MQ09191,0,0,1,-1,,-1,-1,-1,-1,-1,-1,
    DTStyle:3073,0,2147483647,MQ09192,0,0,1,-1,,-1,-1,-1,-1,-1,-1,
    FeatureCollection:2,PointFeature:,3072,0,,,0,45.36333,-68.504058,32767,32767,
    PointFeature:,3073,0,,,0,45.315357,-68.474258,32767,32767,
    PrimitiveCollection:1,LinePrimitive.2:3617,RouteShape,1,255,65280,150,0,
    LatLngCollection.1:7,45363330,-68504058,-6889,-3235,-7343,-9316,-6359,6417,-11261,9460,-14687,23003,-1434,3471,
    PointCollection:0,BestFit.2:1.2,1,DTCollection.1:0,0,0,DisplayState.1:0,72,1,
    Authentication.3:b70fHwouU7>yb446,73655,,
    NET_5.3.0_RC1,2141274732,
    

    As you can see my route from work to home is not very long :-)

  • Building a reusable Windows Control with MapQuest API 5.3 and C#

    Time to take the MapQuest API 5.3 beta a step further and see how hard it is to embed MapQuest mapping a windows application. As a developer I&#39;m constantly thinking "is this a one time effort? or do I want to reuse this code?". Well for embedding MapQuest into my .NET application I would like to create a reusable component, in fact, I&#39;d like to create a WinForm usercontrol which can be reused in any application simply by dragging and dropping the control onto a form.

    So what are the steps to build such a control?

    1. Create a new library project in C#

    2. Add a new item of type UserControl

      add user control
    3. Is intended to display a MapQuest route, for this we need a PictureBox, so add a PictureBox control to the design surface and set the Dock property to Fill. This will make the picture resize automatically to the size of the UserControl.

    4. Since we&#39;ll be accessing the Internet and there is no telling how fast our connection is, we&#39;ll want all the interaction with the MapQuest server to be done on a background thread, so add a BackgroundWorker control to the usercontrol as well.

    5. We want our control to show a &#39;loading&#39; image while it it busy dowloading the map, and we need an error image in case an error occurs and we&#39;re unable to show a map. Create a resource file and add two images for this purpose:
      image resources

    6. Time to write some code. We add a method to the control for showing a route. The method takes three parameters: origin, destination and errorhandler:
      public void ShowRoute( GeoAddress origin, GeoAddress destination, HandleException handler )
      {
      _handler = handler;
      picMap.Image = null;
      try
      {
      Validations.Parameters.NotNull( origin, "origin",
      "Origin cannot be null when showing a route." );
      Validations.Parameters.NotNull( destination, "destination",
      "Destination cannot be null when showing a route." );
      }
      catch ( Exception exception )
      {
      picMap.Image = Images.Error;
      if ( _handler == null )
      {
      throw;
      }
      
      _handler( exception );
      }
      
      RouteData data = new RouteData
      {
      Origin = origin,
      Destination = destination,
      Width = picMap.Width,
      Height = picMap.Height
      };
      BuildMapWorker.RunWorkerAsync( data );
      picMap.Image = Images.Loading;
      }
      

      Notice in the code above we perform some basic parameter checking and then pass the data to the BuildMapWorker thread. We&#39;re using the C# 3.0 object initializer for initializing the values of the struct.
      Also in the code, if the handler parameter is null, then any exception thrown during the MapQuest server interaction will be thrown without handling. HandleException is a delegate allowing the user of the control to decide how to handle any exceptions.
      Right after the work is handed off to the BuildMapWorker thread the image is set to &#39;Loading&#39;.

    7. The RunWorkAsync method on the BackgroundWorker will trigger the code in the DoWork eventhandler. Here we use the helper proxies that we created in our previous post to first crearte a route and the map the route to an actual map.
      private void BuildMapWorker_DoWork( object sender, DoWorkEventArgs e )
      {
      RouteData data = (RouteData) e.Argument;
      
      RouteResults route;
      using ( RouteServerProxy proxy = new RouteServerProxy() )
      {
      route = proxy.CreateRoute( data.Origin, data.Destination );
      }
      using ( MapServerProxy proxy = new MapServerProxy() )
      {
      // e.Result will be a bitmap
      e.Result = proxy.GetMap( route.ShapePoints.ToLine(), data.Width, data.Height );
      }
      }
      

      Note that RouteData is a custom struct for passing information from the UI thread to the background thread. The background thread is not allowed to interact with the UI controls, so all information needs to be passed as a parameter.
      Similarly the result of GetMap is a bitmap, we don&#39;t put the bitmap directly back onto the control, instead we pass it back to the UI thread via the DoWorkEventArgs parameter.

    8. Let&#39;s take a look at the proxy code, there are a couple of overloads but the following method gets called to create the route:
      public RouteResults CreateRoute( LocationCollection addresses )
      {
      // This is the collection that will hold the geocoded locations to be utilized in the call to DoRoute.
      LocationCollection routeLocations = addresses;
      
      // The RouteOptions object contains information pertaining to the Route to be performed.
      RouteOptions routeOptions = new RouteOptions();
      routeOptions.MaxShapePointsPerManeuver = 100;
      
      // The RouteResults object will contain the results of the DoRoute call.  The
      // results contains information such as the narrative, drive time and distance.
      RouteResults routeResults = new RouteResults();
      
      // This call to the server actually generates the route.
      Exec.DoRoute( routeLocations, routeOptions, routeResults, String.Empty );
      
      // Throw an exception if an error occurred.
      ThrowExceptionOnError( routeResults );
      
      return routeResults;
      }
      

    9. Then the code in the MapServerProxy to load the map looks like this:
      public Bitmap GetMap( LinePrimitive route, int width, int height )
      {
      // The MapState object contains the information necessary to display the map,
      // such as size, scale, and latitude/longitude coordinates for centering the map.
      MapState map = new MapState();
      
      // Define the width and height of the map in pixels.
      map.WidthPixels = width;
      map.HeightPixels = height;
      
      // The MapQuest Session object is composed of multiple objects,
      // such as the MapState and CoverageStyle.
      Session session = new Session();
      
      // Add user defined styles in order to create custom points of interest.
      CoverageStyle styles = new CoverageStyle();
      DTStyle start = styles.AddIcon( 3072, Miscellaneous.Start.Value() );
      DTStyle end = styles.AddIcon( 3073, "MQ09192" );
      
      // A point of interest is considered a feature on the map.
      // A map can have multiple features.
      FeatureCollection features = new FeatureCollection();
      features.AddPointFeature( route.LatLngs.First(), start );
      features.AddPointFeature( route.LatLngs.Last(), end );
      
      // Add the line primitive to a primitiveCollection
      PrimitiveCollection primitives = new PrimitiveCollection();
      primitives.Add( route );
      
      // The best fit object is used to draw a map at an appropriate scale determined
      // by the features you have added to the session, along with the optional primitives.
      // In this case we want the map to be displayed at a scale that includes the origin
      // and destination locations (pointFeatures) as well as the routeHighlight(linePrimitive).
      // The scaleAdjustmentFactor is then used to increase the scale by this factor based
      // upon the best fit that was performed.  This results in a border around the edge of
      // the map that does not include any features so the map appears clearer.
      BestFit scaling = new BestFit();
      scaling.ScaleAdjustmentFactor = 1.2;
      scaling.IncludePrimitives = true;
      
      // Add objects to the session.
      session.AddOne( map );
      session.AddOne( styles );
      session.AddOne( features );
      session.AddOne( primitives );
      session.AddOne( scaling );
      
      sbyte[] imageBytes = Exec.GetMapImageDirect( session );
      byte[] bytes = (byte[]) (Array) imageBytes;
      
      MemoryStream stream = new MemoryStream( bytes );
      Bitmap bitmap = new Bitmap( stream );
      
      return bitmap;
      }
      

    10. Lastly, the RunWorkerCompleted event is fired by the BackgroundWorker and here the event arguments now contain the bitmap we want to put on the control:
      private void BuildMapWorker_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e )
      {
      try
      {
      if ( e.Error != null )
      {
      // throw the error on the UI thread.
      throw e.Error;
      }
      else
      {
      // update the image back on the UI thread
      this.picMap.Image = (Bitmap) e.Result;
      }
      }
      catch ( Exception exception )
      {
      // handle the exception on the UI thread (in case the handler implement UI interaction)
      picMap.Image = Images.Error;
      if ( _handler == null )
      {
      throw;
      }
      _handler( exception );
      }
      }
      

    I&#39;ve created a demo application, which actually has a couple more controls, which demos the use of this control. You can download it here, all the sources are included. To make the application run you will need to supply your own ClientID and Password in the app.config file.

    The demo works like this, enter the point of origin on the first tab:

    screenshot 1

    Then enter the destination on the second tab:

    screenshot 2

    After which you can go to the third tab and see the route:

    screenshot 3

    Happy coding!

    - Mark Blomsma

    Download sources here.