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'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'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'll be accessing the Internet and there is no telling how fast our connection is, we'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 'loading' image while it it busy dowloading the map, and we need an error image in case an error occurs and we'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'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 'Loading'.

  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't put the bitmap directly back onto the control, instead we pass it back to the UI thread via the DoWorkEventArgs parameter.

  8. Let'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'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.