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 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;

  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 & 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 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;

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.
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"