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
{
public const int MaxZoom = 18;
public static event EventHandler DownloadCountChanged;
public static event EventHandler DownloadError;
public static string CacheFolder { get; set; }
public static int DownloadCount { get; }
public static string UserAgent { get; set; }
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()
{
TileGenerator.CacheFolder = @"ImageCache";
TileGenerator.UserAgent = "MyDemoApp";
this.InitializeComponent(); }
MapCanvas Class
The actual map is displayed inside the MapCanvas
, which is inherited from the WPF Canvas
control (hence the well thought out name ;) ).
public sealed class MapCanvas : Canvas
{
public static readonly DependencyProperty LatitudeProperty;
public static readonly DependencyProperty LongitudeProperty;
public static readonly DependencyProperty ViewportProperty;
public static readonly DependencyProperty ZoomProperty;
public Rect Viewport { get; }
public int Zoom { get; set; }
public static double GetLatitude(DependencyObject obj);
public static double GetLongitude(DependencyObject obj);
public static void SetLatitude(DependencyObject obj, double value);
public static void SetLongitude(DependencyObject obj, double value);
public void Center(double latitude, double longitude, Size size);
public ImageSource CreateImage();
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:
<!---->
<!---->
<!---->
<map:MapCanvas>
<!---->
<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
{
public event EventHandler SearchCompleted;
public event EventHandler<SearchErrorEventArgs> SearchError;
public SearchResult[] Results { get; }
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
{
public string DisplayName { get; }
public int Index { get; }
public double Latitude { get; }
public double Longitude { get; }
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