Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Map App: WPF Meets Google Geocoding and Static Maps APIs

0.00/5 (No votes)
16 Aug 2011 1  
How to use the Google Geocoding and Static Maps APIs with a WPF application.

Park_1.png

Park_2.png

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, 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:

  1. When the map image is clicked on, a web browser is opened that launches Google Maps for the same location or,
  2. 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:

Map_App_Layout.png

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:

Gigiri_Forest.png

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.

MapImage.png

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here