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:
="1.0" ="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, multiple <result>
elements may be generated. For example, a request with
the address Gigiri, Nairobi results in the following XML data being returned:
="1.0" ="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:
="1.0" ="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 and 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 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, in case of such an occurrence.
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()
Dim diff As Double
Dim shift As Double
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.