Click here to Skip to main content
11,577,965 members (59,946 online)
Click here to Skip to main content

WPF Map Control using openstreetmap.org Data

, 28 Jan 2012 CPOL 132.8K 8.5K 127
Rate this:
Please Sign up or sign in to vote.
A WPF control that displays data from OpenStreetMap
MapControl.png

Introduction

This article will create a simple WPF control that enables browsing of map data from OpenStreetMap, as well as enabling searching for places and displaying the results on the map.

The attached zip file has both the source code, with documentation, and a sample application.

Background

I needed to allow the user to select various locations in my project but didn't want the user to have to install any other applications (such as Google Earth or Bing Maps 3D). One option was to have a web browser in my application pointing to the online versions, but this didn't feel right. Finally, I looked at OpenStreetMap and was impressed by the maps, but couldn't find any controls to put in my application.

What is OpenStreetMap?

From their Main Wiki page:

OpenStreetMap creates and provides free geographic data such as street maps to anyone who wants them. The project was started because most maps you think of as free actually have legal or technical restrictions on their use, holding back people from using them in creative, productive, or unexpected ways.

Basically, OpenStreetMap is a map made by the community for everybody to use. Also, luckily for me, all the details for the file naming conventions are there for creating our own control. All we have to do is download the relevant 256 x 256 pixel square image tiles for the area we want to look at and stitch them together - simple!

Performance

The original version of the code used a method similar to the WinForms DoEvents method to allow queued up messages in the UI to be processed while the images were updating. There was a reason WPF doesn't implement this method because it's not a good idea to do that! This time around, there's a separate class responsible for fetching the images (either from the cache on disk or from the server) and this is called on a background thread (using ThreadPool.QueueUserWorkItem to take care of thread creation) and then freezing the BitmapImage. Once frozen, this can be passed back to the UI thread, which updates one tile at a time as needed without blocking.

Using the Code

All the basic functionality for displaying a map and searching is in the MapControl project, however, you will need to create the controls for navigating and for getting the search query from the user. I've made a separate project called SampleApp which does just this, but I must confess my design skills suck.

TileGenerator Class

This class has helper methods to retrieve information from OpenStreetMap and has the following members:

public static class TileGenerator
{
    // The maximum allowed zoom level.
    public const int MaxZoom = 18;

    // Occurs when the number of downloads changes.
    public static event EventHandler DownloadCountChanged;

    // Occurs when there is an error downloading a Tile.
    public static event EventHandler DownloadError;

    // Gets or sets the folder used to store the downloaded tiles.
    // Note: This should be set before any call to GetTileImage.
    public static string CacheFolder { get; set; }

    // Gets the number of tile images waiting to be downloaded.
    // Note: this is not the same as the number of active downloads.
    public static int DownloadCount { get; }
    
    // Gets or sets the user agent used to make the tile request.
    // Note: This should be set before any call to GetTileImage.
    public static string UserAgent { get; set; }

    // Returns a valid value for the specified zoom, 
    // in the range of 0 - MaxZoom inclusive.
    public static int GetValidZoom(int zoom);
}

Before we do anything with any of the map controls, even before trying to call their constructors, we need to set the directory for the tile image cache folder and the user agent we will use to identify ourselves. Here MainWindow is assumed to be the first window loaded but you could instead put this line inside the constructor of the default App class:

public MainWindow()
{
    // Very important we set the CacheFolder before doing anything so the
    // MapCanvas knows where to save the downloaded files to.
    TileGenerator.CacheFolder = @"ImageCache";
    TileGenerator.UserAgent = "MyDemoApp";

    this.InitializeComponent(); // Because this will create the MapCanvas, it
                                // has to go after the above.
}

MapCanvas Class

The actual map is displayed inside the MapCanvas, which is inherited from the WPF Canvas control (hence the well thought out name Wink | ;) ).

public sealed class MapCanvas : Canvas
{
    // Identifies the Latitude attached property.
    public static readonly DependencyProperty LatitudeProperty;

    // Identifies the Longitude attached property.
    public static readonly DependencyProperty LongitudeProperty;

    // Identifies the Viewport dependency property. This will be read only.
    public static readonly DependencyProperty ViewportProperty;

    // Identifies the Zoom dependency property.
    public static readonly DependencyProperty ZoomProperty;

    // Gets the visible area of the map in latitude/longitude coordinates.
    public Rect Viewport { get; }

    // Gets or sets the zoom level of the map.
    public int Zoom { get; set; }

    // Gets the value of the Latitude attached property for a given dependency object.
    public static double GetLatitude(DependencyObject obj);

    // Gets the value of the Longitude attached property for a given dependency object.
    public static double GetLongitude(DependencyObject obj);

    // Sets the value of the Latitude attached property for a given dependency object.
    public static void SetLatitude(DependencyObject obj, double value);

    // Sets the value of the Longitude attached property for a given dependency object.
    public static void SetLongitude(DependencyObject obj, double value);

    // Centers the map on the specified coordinates, calculating the required zoom level.
    // The size parameter is the minimum size that must be visible, 
    // centered on the coordinates.
    // i.e. the longitude range that must be visible will be: 
    // longitude +- (size.Width / 2)
    public void Center(double latitude, double longitude, Size size);

    // Creates a static image of the current view.
    public ImageSource CreateImage();

    // Calculates the coordinates of the specified point.
    // The point should be in pixels, relative to the top left corner of the control.
    // The returned Point will be filled with the Latitude in the Y property and
    // the Longitude in the X property.
    public Point GetLocation(Point point);
}

The main points of interest are the two attached properties that make it a bit easier for positioning child controls (though you can still use the regular Canvas ones such as Canvas.Left, etc.): MapCanvas.Latitude and MapCanvas.Longitude. Using them should be straight forward:

<!-- Make sure you've included a reference to MapControl.dll in your project -->
<!-- and put a reference like the following at the start of the XAML file: -->
<!-- xmlns:map="clr-namespace:MapControl;assembly=MapControl" -->

<map:MapCanvas>
  <!-- The Top Left corner of the control will be at the specified Latitude + Longitude,
  so set a negative Margin to centralise the control on the coordinates. -->
  <Rectangle Fill="Red" Height="50" Width="50" Margin="-25,-25,0,0"
             map:MapCanvas.Latitude="38.895" map:MapCanvas.Longitude="-77.037" />
</map:MapCanvas>

Panning and Zooming

The MapCanvas will handle dragging with the mouse and zooming using the scroll wheel, however, you will probably want to add a set of navigation controls as well. To enable this, the MapControl registers itself with the following (self explanatory) standard WPF commands:

  • ComponentCommands.MoveDown
  • ComponentCommands.MoveLeft
  • ComponentCommands.MoveRight
  • ComponentCommands.MoveUp
  • NavigationCommands.DecreaseZoom
  • NavigationCommands.IncreaseZoom

SearchProvider/SearchResult Classes

This SearchProvider class first tries to parse the query for a decimal latitude and longitude (in that order, separated by a comma and/or space) but if that fails will pass the query on to Nominatim to search osm data by name and address. Just to reiterate, it will only try and parse decimal degrees, not degrees minutes seconds.

public sealed class SearchProvider
{
    // Occurs when the search has completed.
    public event EventHandler SearchCompleted;

    // Occurs if there were errors during the search.
    public event EventHandler<SearchErrorEventArgs> SearchError;

    // Gets the results returned from the most recent search.
    public SearchResult[] Results { get; }

    // Searches for the specified query, localizing the results to the specified
    // area.
    // Note that it only returns true if a search was started. This does not
    // always mean that the method has failed - if a set of valid coordinates
    // were passed as the query then no search will be performed (returning
    // false) but the SearchCompleted event will be raised and the Results will
    // be updated.
    public bool Search(string query, Rect area);
}

This finally leaves the SearchResult class that, as you would expect, contains information for an individual search result returned from Nominatim.

public sealed class SearchResult
{
    // Gets the formatted name of the search result.
    public string DisplayName { get; }

    // Gets the index the result was returned from the search.
    public int Index { get; }

    // Gets the latitude coordinate of the center of the search result.
    public double Latitude { get; }

    // Gets the longitude coordinate of the center of the search result.
    public double Longitude { get; }

    // Gets the size of the search's bounding box.
    public System.Windows.Size Size { get; }
}

Points of Interest

Before using the code or sample application, you should read and make sure you comply with the following:

The way I read it is make sure you put a copyright notice on the map (like the one in the bottom right hand corner of the sample application) and make sure you don't abuse the servers by downloading too much (such as trying to download all the tiles in one go).

History

  • 27/01/2012 - Allowed the user agent to be specified (complying with the Tile Usage Policy) and removed the DoEvents related code
  • 15/06/2010 - Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Samuel Cragg
United Kingdom United Kingdom
No Biography provided

You may also be interested in...

Comments and Discussions

 
GeneralRe: Bug with wrong tiles being displayed Pin
tim_smith12-Jul-13 4:59
membertim_smith12-Jul-13 4:59 
BugSearchPovider Pin
ArsenyTemp19-Apr-13 0:34
memberArsenyTemp19-Apr-13 0:34 
GeneralBrilliant control Pin
Member 942776811-Mar-13 23:13
memberMember 942776811-Mar-13 23:13 
GeneralMy vote of 5 Pin
maapoto24-Jan-13 13:53
membermaapoto24-Jan-13 13:53 
QuestionBug with mouse moving Pin
hdr269-Jan-13 8:31
memberhdr269-Jan-13 8:31 
AnswerRe: Bug with mouse moving Pin
hdr2610-Jan-13 10:31
memberhdr2610-Jan-13 10:31 
QuestionMap Scale Bar Pin
jed_codeproject9-Jan-13 4:14
memberjed_codeproject9-Jan-13 4:14 
QuestionDo you plan a windows store app version of this control? Pin
elsni6-Dec-12 8:32
memberelsni6-Dec-12 8:32 
QuestionHow to calculate Lat/Lon-s from mouse position and reverse? Pin
gyuri102-Nov-12 2:49
membergyuri102-Nov-12 2:49 
AnswerRe: How to calculate Lat/Lon-s from mouse position and reverse? Pin
Sam Cragg2-Nov-12 8:26
memberSam Cragg2-Nov-12 8:26 
Question2 bugs and their fixes Pin
gyuri1031-Oct-12 10:05
membergyuri1031-Oct-12 10:05 
AnswerRe: 2 bugs and their fixes Pin
Sam Cragg31-Oct-12 12:27
memberSam Cragg31-Oct-12 12:27 
AnswerRe: 2 bugs and their fixes Pin
Petr Gotthard11-Mar-13 23:48
memberPetr Gotthard11-Mar-13 23:48 
QuestionA Problem about Center method. Pin
Adam_cz27-Oct-12 5:54
memberAdam_cz27-Oct-12 5:54 
AnswerRe: A Problem about Center method. Pin
Adam_cz27-Oct-12 7:54
memberAdam_cz27-Oct-12 7:54 
QuestionSlow down fix Pin
turnerc13-May-12 7:37
memberturnerc13-May-12 7:37 
AnswerRe: Slow down fix Pin
Sam Cragg13-May-12 10:31
memberSam Cragg13-May-12 10:31 
QuestionA problem about MapCanvas(MapControl) Pin
pandagxnu24-Apr-12 20:08
memberpandagxnu24-Apr-12 20:08 
AnswerRe: A problem about MapCanvas(MapControl) Pin
tDanielOliveira13-May-12 22:39
membertDanielOliveira13-May-12 22:39 
AnswerRe: A problem about MapCanvas(MapControl) Pin
gBfr28-Jun-12 0:59
membergBfr28-Jun-12 0:59 
SuggestionRe: A problem about MapCanvas(MapControl) Pin
Member 78167733-Jul-12 22:33
memberMember 78167733-Jul-12 22:33 
AnswerRe: A problem about MapCanvas(MapControl) Pin
gyuri1031-Oct-12 10:09
membergyuri1031-Oct-12 10:09 
GeneralMy vote of 5 Pin
Saed Leghaee15-Apr-12 22:35
memberSaed Leghaee15-Apr-12 22:35 
QuestionQuestion Lines Pin
panamaju21-Mar-12 14:53
memberpanamaju21-Mar-12 14:53 
AnswerRe: Question Lines Pin
Sam Cragg21-Mar-12 23:48
memberSam Cragg21-Mar-12 23:48 
SuggestionRe: Question Lines Pin
Dr. Jones DK12-Jun-12 22:04
memberDr. Jones DK12-Jun-12 22:04 
GeneralRe: Question Lines Pin
Sam Cragg13-Jun-12 7:40
memberSam Cragg13-Jun-12 7:40 
QuestionOpenstreet maps for Asp.net Pin
LouisGia29-Feb-12 2:29
memberLouisGia29-Feb-12 2:29 
AnswerRe: Openstreet maps for Asp.net Pin
Sam Cragg29-Feb-12 3:06
memberSam Cragg29-Feb-12 3:06 
GeneralMy vote of 5 Pin
Shahin Khorshidnia29-Jan-12 9:29
memberShahin Khorshidnia29-Jan-12 9:29 
GeneralMy vote of 5 Pin
Espiritu28-Jan-12 16:08
memberEspiritu28-Jan-12 16:08 
QuestionUser-Agent required by OpenStreetMaps Pin
michalJ25-Jan-12 8:02
membermichalJ25-Jan-12 8:02 
AnswerRe: User-Agent required by OpenStreetMaps Pin
Sam Cragg25-Jan-12 11:52
memberSam Cragg25-Jan-12 11:52 
AnswerRe: User-Agent required by OpenStreetMaps Pin
Sam Cragg27-Jan-12 12:15
memberSam Cragg27-Jan-12 12:15 
I've made the changes to the code, so when the article gets updated over the next few days you'll be able to specify the user agent (it's a property on the TileGenerator class.) You're probably best to specify it where you specify the cache folder, like so:
TileGenerator.CacheFolder = // As before
TileGenerator.UserAgent = "MyDemoApp";
 
// Now the rest of the code...
this.InitializeComponent();

QuestionRepositionChildren called far too often Pin
chris rothery1-Sep-11 3:51
memberchris rothery1-Sep-11 3:51 
AnswerRe: RepositionChildren called far too often Pin
Sam Cragg1-Sep-11 8:12
memberSam Cragg1-Sep-11 8:12 
QuestionHelp me plz... I want to get some real coordinates from a given list of screen coordinates Pin
huan doan5-Aug-11 20:36
memberhuan doan5-Aug-11 20:36 
AnswerRe: Help me plz... I want to get some real coordinates from a given list of screen coordinates Pin
Sam Cragg6-Aug-11 6:10
memberSam Cragg6-Aug-11 6:10 
GeneralRe: Help me plz... I want to get some real coordinates from a given list of screen coordinates Pin
huan doan9-Aug-11 5:43
memberhuan doan9-Aug-11 5:43 
GeneralRe: Help me plz... I want to get some real coordinates from a given list of screen coordinates Pin
Sam Cragg9-Aug-11 13:32
memberSam Cragg9-Aug-11 13:32 
GeneralRe: Help me plz... I want to get some real coordinates from a given list of screen coordinates Pin
huan doan9-Aug-11 16:29
memberhuan doan9-Aug-11 16:29 
GeneralRe: Help me plz... I want to get some real coordinates from a given list of screen coordinates Pin
Sam Cragg10-Aug-11 4:20
memberSam Cragg10-Aug-11 4:20 
QuestionHow to add 500 Random Markers on Openstreet map Pin
vivek260826-Jun-11 23:12
membervivek260826-Jun-11 23:12 
AnswerRe: How to add 500 Random Markers on Openstreet map Pin
Sam Cragg27-Jun-11 4:01
memberSam Cragg27-Jun-11 4:01 
GeneralRe: How to add 500 Random Markers on Openstreet map Pin
vivek260827-Jun-11 6:05
membervivek260827-Jun-11 6:05 
GeneralRe: How to add 500 Random Markers on Openstreet map Pin
Sam Cragg27-Jun-11 6:38
memberSam Cragg27-Jun-11 6:38 
GeneralRe: How to Call Pan on map on button click Pin
vivek260829-Jun-11 22:35
membervivek260829-Jun-11 22:35 
GeneralRe: How to Call Pan on map on button click Pin
Sam Cragg30-Jun-11 4:04
memberSam Cragg30-Jun-11 4:04 
GeneralRe: How to Call Pan on map on button click Pin
vivek26085-Jul-11 22:53
membervivek26085-Jul-11 22:53 
GeneralRe: How to Call Pan on map on button click Pin
Sam Cragg6-Jul-11 4:45
memberSam Cragg6-Jul-11 4:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150603.1 | Last Updated 28 Jan 2012
Article Copyright 2010 by Samuel Cragg
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid