WPF Map App: WPF meets Google Geocoding & Static Maps APIs
Introduction
Google provides web and mobile developers with several APIs that can be used to enhance the user experience on a website or mobile application. In this article I will explain how you can go about using the Google Geocoding and Static Maps APIs to display Google Maps in a WPF application.
Background
Displaying Google Maps in a WPF application can involve using the Google Maps API for Flash or the Google Maps JavaScript API. In the former approach you would need to embed a Flash Player control in your WPF application, a task that would require you to be conversant with ActionScript since it involves creation of a SWF file using the Adobe Flex SDK; You can read more on how to do this here.
The latter approach, using the Google Maps JavaScript API, is the easier of the two options but requires familiarity with JavaScript and involves the use of the WebBrowser
control to display a Google Map.
The approach taken in this article avoids either ActionScript or JavaScript allowing the use of a .NET language as the only channel to interact with two Google APIs; Google Geocoding API and the Static Maps API, to display geographical coordinates and a Google Map of a user specified address. The sample application enables the user to zoom-in or out of the target address, change map types, scroll the map, and save an image of the map at the current map type and zoom level. Clicking on the map opens Google Maps in Internet Explorer with the requested address as the target location.
Requirements
You require an internet connection to make use of Geocoding and Static Maps APIs.
Google Geocoding API (V3)
Geocoding is the process of converting addresses into geographical coordinates or vice versa (reverse geocoding). The Google Geocoding API returns json or xml data containing details of a requested location. A request to the Geocoding API should be of the following form;
http://maps.googleapis.com/maps/api/geocode/output?parameters
For example, to get xml data for Uhuru Park, Nairobi the URL will be as follows;
http://maps.googleapis.com/maps/api/geocode/xml?address=Uhuru+Park,+Nairobi&sensor=false
The sensor
parameter indicates that the geocoding request does not come from a device with a location sensor.
The xml data that will be returned by this request is;
<?xml version="1.0" encoding="utf-8"?>
<GeocodeResponse>
<status>OK</status>
<result>
<type>park</type>
<type>park</type>
<type>establishment</type>
<formatted_address>Uhuru Park, Kenyatta Ave, Nairobi, Kenya</formatted_address>
<address_component>
<long_name>Uhuru Park</long_name>
<short_name>Uhuru Park</short_name>
<type>establishment</type>
</address_component>
<address_component>
<long_name>Kenyatta Ave</long_name>
<short_name>Kenyatta Ave</short_name>
<type>route</type>
</address_component>
<address_component>
<long_name>Kilimani</long_name>
<short_name>Kilimani</short_name>
<type>sublocality</type>
<type>political</type>
</address_component>
<address_component>
<long_name>Nairobi</long_name>
<short_name>Nairobi</short_name>
<type>locality</type>
<type>political</type>
</address_component>
<address_component>
<long_name>Nairobi</long_name>
<short_name>Nairobi</short_name>
<type>administrative_area_level_2</type>
<type>political</type>
</address_component>
<address_component>
<long_name>Nairobi</long_name>
<short_name>Nairobi</short_name>
<type>administrative_area_level_1</type>
<type>political</type>
</address_component>
<address_component>
<long_name>Kenya</long_name>
<short_name>KE</short_name>
<type>country</type>
<type>political</type>
</address_component>
<geometry>
<location>
<lat>-1.2899952</lat>
<lng>36.8159383</lng>
</location>
<location_type>APPROXIMATE</location_type>
<viewport>
<southwest>
<lat>-1.3011503</lat>
<lng>36.7999309</lng>
</southwest>
<northeast>
<lat>-1.2788400</lat>
<lng>36.8319457</lng>
</northeast>
</viewport>
<bounds>
<southwest>
<lat>-1.2932307</lat>
<lng>36.8118851</lng>
</southwest>
<northeast>
<lat>-1.2867596</lat>
<lng>36.8199916</lng>
</northeast>
</bounds>
</geometry>
</result>
</GeocodeResponse>
In the case where the geocoder can only match part of the requested address then multiple <result>
elements may be generated. For example, a request with the address Gigiri, Nairobi results in the following xml data being returned;
<?xml version="1.0" encoding="utf-8"?>
<GeocodeResponse>
<status>OK</status>
<result>
<type>park</type>
<type>park</type>
<type>establishment</type>
<formatted_address>Gigiri Forest, Nairobi, Kenya</formatted_address>
<address_component>
<long_name>Gigiri Forest</long_name>
<short_name>Gigiri Forest</short_name>
<type>establishment</type>
</address_component>
...
<partial_match>true</partial_match>
</result>
<result>
<type>route</type>
<formatted_address>Gigiri Rd, Nairobi, Kenya</formatted_address>
<address_component>
<long_name>Gigiri Rd</long_name>
<short_name>Gigiri Rd</short_name>
<type>route</type>
</address_component>
...
<partial_match>true</partial_match>
</result>
</GeocodeResponse>
In the case where the geocode was successful but no results were returned the xml data will be as follows;
<?xml version="1.0" encoding="utf-8"?>
<GeocodeResponse>
<status>ZERO_RESULTS</status>
</GeocodeResponse>
For a detailed explanation on how to go about using the Geocoding API read through the Google Geocoding API documentation.
Google Static Maps API (V2)
The Google Static Maps API enables you to embed a Google Maps image in a webpage and you can also do the same for a desktop application. The Static Maps API returns an image in either GIF, PNG (default), or JPEG format.
A request to this API should be in the following form;
http://maps.googleapis.com/maps/api/staticmap?parameters
For example, to get the Google Maps image for Uhuru Park, Nairobi the URL will be;
http://maps.googleapis.com/maps/api/staticmap?size=500x400
&markers=size:mid%7Ccolor:red%7CUhuru+Park,+Nairobi
&zoom=15&sensor=false
The markers
parameter specifies a set of one or more markers at a set location(s). The markers
parameter takes a set of value assignments (marker descriptors).
For a detailed description on how to go about using the Static Maps API check out the Google Static Maps API developer's guide.
Note: Developers are permitted to use the Static Maps API outside of a web browser provided that the map image is linked to Google Maps. You should ensure that either;
- When the map image is clicked on, a web browser is opened that launches Google Maps for the same location or,
- you add a link under your image that says "Open in Google Maps" or "View in Google Maps" that opens a web browser.
Details regarding the use of the Static Maps API outside of a web browser are specified in Section 10.1.1(h) of the Google Maps Terms of Service.
The Google Maps URL format is documented here.
WPF Map App
Design & Layout
I designed the sample application in Expression Blend. The following image shows some elements of interest,
The Code
When the user enters an address in the AddressTxtBox
and clicks on the Show button or presses the Enter key, ShowMapButton
's Click
event handler is called.
Private Sub ShowMapButton_Click(ByVal sender As Object, _
ByVal e As System.Windows.RoutedEventArgs) _
Handles ShowMapButton.Click
If (AddressTxtBox.Text <> String.Empty) Then
location = AddressTxtBox.Text.Replace(" ", "+")
zoom = 15
mapType = "roadmap"
Dim geoThread As New Thread(AddressOf GetGeocodeData)
geoThread.Start()
ShowMapImage()
AddressTxtBox.SelectAll()
ShowMapButton.IsEnabled = False
MapProgressBar.Visibility = Windows.Visibility.Visible
If (RoadmapToggleButton.IsChecked = False) Then
RoadmapToggleButton.IsChecked = True
TerrainToggleButton.IsChecked = False
End If
Else
MessageBox.Show("Enter location address.", _
"Map App", MessageBoxButton.OK, MessageBoxImage.Exclamation)
AddressTxtBox.Focus()
End If
End Sub
The GetGeocodeData()
method, that is called on a background thread, sets the value of an XDocument
variable with data returned by the Geocoding API.
Private Sub GetGeocodeData()
Dim geocodeURL As String = "http://maps.googleapis.com/maps/api/" & _
"geocode/xml?address=" & location & "&sensor=false"
Try
geoDoc = XDocument.Load(geocodeURL)
Catch ex As WebException
Me.Dispatcher.BeginInvoke(New ThreadStart(AddressOf HideProgressBar), _
DispatcherPriority.Normal, Nothing)
MessageBox.Show("Ensure that internet connection is available.", _
"Map App", MessageBoxButton.OK, MessageBoxImage.Error)
Exit Sub
End Try
Me.Dispatcher.BeginInvoke(New ThreadStart(AddressOf ShowGeocodeData), _
DispatcherPriority.Normal, Nothing)
End Sub
The ShowGeocodeData()
method updates the values of the necessary UI elements.
Private Sub ShowGeocodeData()
Dim responseStatus = geoDoc...<status>.Single.Value()
If (responseStatus = "OK") Then
Dim formattedAddress = geoDoc...<formatted_address>(0).Value()
Dim latitude = geoDoc...<location>(0).Element("lat").Value()
Dim longitude = geoDoc...<location>(0).Element("lng").Value()
Dim locationType = geoDoc...<location_type>(0).Value()
AddressTxtBlck.Text = formattedAddress
LatitudeTxtBlck.Text = latitude
LongitudeTxtBlck.Text = longitude
Select Case locationType
Case "APPROXIMATE"
AccuracyTxtBlck.Text = "Approximate"
Case "ROOFTOP"
AccuracyTxtBlck.Text = "Precise"
Case Else
AccuracyTxtBlck.Text = "Approximate"
End Select
lat = Double.Parse(latitude)
lng = Double.Parse(longitude)
If (SaveButton.IsEnabled = False) Then
SaveButton.IsEnabled = True
RoadmapToggleButton.IsEnabled = True
TerrainToggleButton.IsEnabled = True
End If
ElseIf (responseStatus = "ZERO_RESULTS") Then
MessageBox.Show("Unable to show results for: " & vbCrLf & _
location, "Unknown Location", MessageBoxButton.OK, _
MessageBoxImage.Information)
DisplayXXXXXXs()
AddressTxtBox.SelectAll()
End If
ShowMapButton.IsEnabled = True
ZoomInButton.IsEnabled = True
ZoomOutButton.IsEnabled = True
MapProgressBar.Visibility = Windows.Visibility.Hidden
End Sub
In the method above I'm making use of LINQ to XML and XML Axis properties to get the required details from geoDoc
. Note the use of the index axis property, (0)
. I use it to get the first element in the returned sequences since the Google Static Map API will only return the map image of the first partial match, incase of such an occurence. In the case of the previous example of Gigiri, Nairobi, the result would be;
ShowMapImage()
gets and displays the returned Google Map image.
Private Sub ShowMapImage()
Dim bmpImage As New BitmapImage()
Dim mapURL As String = "http://maps.googleapis.com/maps/api/staticmap?" & _
"size=500x400&markers=size:mid%7Ccolor:red%7C" & _
location & "&zoom=" & zoom & "&maptype=" & mapType & "&sensor=false"
bmpImage.BeginInit()
bmpImage.UriSource = New Uri(mapURL)
bmpImage.EndInit()
MapImage.Source = bmpImage
End Sub
Zooming-in on the target address is done by calling the ZoomIn()
method.
Private Sub ZoomIn()
If (zoom < 21) Then
zoom += 1
ShowMapUsingLatLng()
If (ZoomOutButton.IsEnabled = False) Then
ZoomOutButton.IsEnabled = True
End If
Else
ZoomInButton.IsEnabled = False
End If
End Sub
The ShowMapUsingLatLng()
method is similar to ShowMapImage()
the difference being that in the former the center of the map, requested from the Static Maps API, is set using the center
parameter with latitude and longitude values. This approach proves most useful when scrolling the map with the arrow buttons.
Private Sub ShowMapUsingLatLng()
Dim bmpImage As New BitmapImage()
Dim mapURL As String = "http://maps.googleapis.com/maps/api/staticmap?" & _
"center=" & lat & "," & lng & "&" & _
"size=500x400&markers=size:mid%7Ccolor:red%7C" & _
location & "&zoom=" & zoom & "&maptype=" & mapType & "&sensor=false"
bmpImage.BeginInit()
bmpImage.UriSource = New Uri(mapURL)
bmpImage.EndInit()
MapImage.Source = bmpImage
End Sub
Clicking on the up arrow button calls the MoveUp()
method.
Private Sub MoveUp()
' Default zoom is 15 and at this level changing
' the center point is done by 0.003 degrees.
' Shifting the center point is done by higher values
' at zoom levels less than 15.
Dim diff As Double
Dim shift As Double
' Use 88 to avoid values beyond 90 degrees of lat.
If (lat < 88) Then
If (zoom = 15) Then
lat += 0.003
ElseIf (zoom > 15) Then
diff = zoom - 15
shift = ((15 - diff) * 0.003) / 15
lat += shift
Else
diff = 15 - zoom
shift = ((15 + diff) * 0.003) / 15
lat += shift
End If
ShowMapUsingLatLng()
Else
lat = 90
End If
End Sub
Switching the maptype
from roadmap
to terrain
is done by the Checked
event handler of TerrainToggleButton
.
Private Sub TerrainToggleButton_Checked(ByVal sender As Object, _
ByVal e As System.Windows.RoutedEventArgs) _
Handles TerrainToggleButton.Checked
If (mapType <> "terrain") Then
mapType = "terrain"
ShowMapUsingLatLng()
RoadmapToggleButton.IsChecked = False
End If
End Sub
To save the map that is currently shown, at the current zoom level, the SaveMap()
method is called.
Private Sub SaveMap()
Dim mapURL As String = "http://maps.googleapis.com/maps/api/staticmap?" & _
"center=" & lat & "," & lng & "&" & _
"size=500x400&markers=size:mid%7Ccolor:red%7C" & _
location & "&zoom=" & zoom & "&maptype=" & mapType & "&sensor=false"
Dim webClient As New WebClient()
Try
Dim imageBytes() As Byte = webClient.DownloadData(mapURL)
Using ms As New MemoryStream(imageBytes)
Image.FromStream(ms).Save(saveDialog.FileName, Imaging.ImageFormat.Png)
End Using
Catch ex As WebException
MessageBox.Show("Unable to save map. Ensure that you are" & _
" connected to the internet.", "Error!", _
MessageBoxButton.OK, MessageBoxImage.Stop)
Exit Sub
End Try
End Sub
The map image will be saved in PNG format at size 500x400.
Note: It is okay to allow the user to save a map for personal use however if you enable sharing of the image over email or social networks it must be by sharing the URL to the Static Map.
Appreciation
Thanks to Thor Mitchell, Product Manager, Google Maps API, who provided insightful feedback on the Terms of Service for the Static Maps API and on saving of Static Map images.
Thanks also to Marc Ridey, Google Geo Team.
Conclusion
That's it. I hope that the information you gathered from this article will prove to be useful.
History
- 9th Aug, 2011: Initial post.
- 11th Aug, 2011: Added zoom, map type, and save features.
- 12th Aug, 2011: Added scrolling feature.
- 16th Aug, 2011: Added feature to enable opening of Google Maps in browser as per Google Maps Terms of Service.
Post Comment
mbyXJN Valuable information. Lucky me I found your site by accident, and I am shocked why this accident did not happened earlier! I bookmarked it.
5VlLo4 Muchos Gracias for your blog.Really looking forward to read more. Awesome.
5LPfvp Thank you ever so for you post.Really thank you! Want more.
HcwHPB I think other site proprietors should take this website as an model, very clean and fantastic user genial style and design, as well as the content. You are an expert in this topic!
rBPktu since you most certainly possess the gift.
rrGClM Thanks a lot for the blog.Really thank you! Awesome.
nAwppa I was able to find good advice from your content.
BjfsqC In my opinion you are not right. I am assured. Write to me in PM, we will discuss.
LobSee You made some good points there. I looked on the web to learn more about the issue and found most people will go along with your views on this web site.
DRK7PF There may be noticeably a bundle to find out about this. I assume you made sure good points in options also.
lYqdNc Thanks for the article.Really looking forward to read more. Really Cool.
ukUJhq wonderful points altogether, you just won a new reader. What would you recommend about your post that you made some days ago? Any sure?
lsvEqR Well I really liked reading it. This post provided by you is very constructive for good planning.
nLCuGI Recommeneded websites Here you all find some sites that we think you all appreciate, just click the links over
1R4NmS I reckon something truly special in this internet site.
UjwCaG Really enjoyed this article post. Really Cool.
BUcrDI This is one awesome blog post.Much thanks again. Keep writing.
XrXUIe Informative article, exactly what I needed.
6EVwtG It is actually difficult to acquire knowledgeable folks using this subject, but the truth is could be observed as did you know what you are referring to! Thanks
It?s actually a great and helpful piece of info. I am satisfied that you shared this useful info with us. Please keep us informed like this. Thank you for sharing.
BCtJun You can certainly see your enthusiasm within the work you write.
ABdh6a Very very good publish, thank that you simply lot regarding sharing. Do you happen a great RSS feed I can subscribe to be able to?
ZBieIi to my friends. I am confident they will be
8j0jse They can get more such compact discs they will captured at home for me to give attention to for in
2ThHZ1
ptiAr5 Thanks-a-mundo for the blog.Really looking forward to read more. Fantastic.
3XosAO This actually is definitely helpful post. With thanks for the passion to present this kind of helpful suggestions here.
DGu4YJ Would you be considering exchanging links?
THt0SN It is really a nice and useful piece of info. I'm glad that you shared this helpful info with us. Please keep us up to date like this. Thank you for sharing.
h418kB Very good written article. It will be beneficial to anybody who usess it, as well as yours truly :). Keep up the good work - looking forward to more posts.
mORNrx You can certainly see your skills in the work you write. The world hopes for even more passionate writers like you who aren't afraid to say how they believe. Always go after your heart.
kSZy5o Usually I don't read post on blogs, but I wish to say that this write-up very forced me to try and do it! Your writing style has been amazed me. Thanks, very nice article.
XTFAVU Say, you got a nice blog post.Much thanks again. Great.
HUaOWa Major thanks for the article post.Really thank you! Great.