WPF 地图应用:WPF 遇见 Google 地理编码和静态地图 API
4.81/5 (21投票s)
如何将 Google 地理编码和静态地图 API 与 WPF 应用程序结合使用。


引言
Google 为 Web 和移动开发者提供了多种 API,可用于增强网站或移动应用程序的用户体验。在本文中,我将解释如何使用 Google 的地理编码和静态地图 API 在 WPF 应用程序中显示 Google 地图。
背景
在 WPF 应用程序中显示 Google 地图可能需要使用Google Maps API for Flash或Google Maps JavaScript API。在前一种方法中,您需要将 Flash Player 控件嵌入到 WPF 应用程序中,这需要您熟悉ActionScript,因为这涉及到使用Adobe Flex SDK创建 SWF 文件;您可以在此处了解更多关于如何执行此操作的信息。
后一种方法,即使用 Google Maps JavaScript API,是这两种选择中较容易的一种,但需要熟悉 JavaScript,并涉及使用 WebBrowser 控件来显示 Google 地图。
本文采用的方法避免了 ActionScript 或 JavaScript,允许仅使用 .NET 语言作为与两个 Google API(Google 地理编码 API 和静态地图 API)交互的唯一途径,以显示地理坐标和用户指定的地址的 Google 地图。示例应用程序使用户能够放大或缩小目标地址、更改地图类型、滚动地图以及在当前地图类型和缩放级别下保存地图图像。点击地图将在 Internet Explorer 中打开 Google Maps,并将请求的地址作为目标位置。
要求
您需要互联网连接才能使用地理编码和静态地图 API。
Google 地理编码 API (V3)
地理编码是将地址转换为地理坐标或反之(反向地理编码)的过程。Google 地理编码 API 返回包含所请求位置详细信息的 JSON 或 XML 数据。对地理编码 API 的请求应为以下形式:
http://maps.googleapis.com/maps/api/geocode/output?parameters
例如,要获取 Uhuru Park, Nairobi 的 XML 数据,URL 将如下所示:
http://maps.googleapis.com/maps/api/geocode/xml?address=Uhuru+Park,+Nairobi&sensor=false
sensor 参数指示地理编码请求不来自带有位置传感器的设备。
此请求将返回的 XML 数据是:
<?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>
如果地理编码器只能匹配请求地址的一部分,可能会生成多个 <result> 元素。例如,对地址 Gigiri, Nairobi 的请求将返回以下 XML 数据:
<?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>
如果地理编码成功但未返回任何结果,XML 数据将如下所示:
<?xml version="1.0" encoding="utf-8"?>
<GeocodeResponse>
<status>ZERO_RESULTS</status>
</GeocodeResponse>
有关如何使用地理编码 API 的详细说明,请阅读 Google 地理编码 API 文档。
Google 静态地图 API (V2)
Google 静态地图 API 使您能够在网页中嵌入 Google 地图图像,您也可以在桌面应用程序中执行相同的操作。静态地图 API 以 GIF、PNG(默认)或 JPEG 格式返回图像。
对该 API 的请求应为以下形式:
http://maps.googleapis.com/maps/api/staticmap?parameters
例如,要获取 Uhuru Park, Nairobi 的 Google 地图图像,URL 将是:
http://maps.googleapis.com/maps/api/staticmap?size=500x400
&markers=size:mid%7Ccolor:red%7CUhuru+Park,+Nairobi
&zoom=15&sensor=false
markers 参数指定在某个位置(或多个位置)设置一个或多个标记。markers 参数接受一组值分配(标记描述符)。
有关如何使用静态地图 API 的详细说明,请查看 Google 静态地图 API 开发者指南。
注意:开发者允许在 Web 浏览器之外使用静态地图 API,前提是地图图像链接到 Google Maps。您应确保以下任一操作:
- 当地图图像被点击时,会打开一个 Web 浏览器,该浏览器启动 Google Maps 并显示相同的位置;或者,
- 在图像下方添加一个链接,写着“在 Google Maps 中打开”或“在 Google Maps 中查看”,该链接会打开一个 Web 浏览器。
有关在 Web 浏览器之外使用静态地图 API 的详细信息,请参阅Google Maps 服务条款的第 10.1.1(h) 条。
Google Maps URL 格式在此 处有文档记录。
WPF 地图应用
设计和布局
我在 Expression Blend 中设计了示例应用程序。下图显示了一些有趣的元素:

代码
当用户在 AddressTxtBox 中输入地址并点击 Show 按钮或按 Enter 键时,将调用 ShowMapButton 的 Click 事件处理程序。
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
在后台线程调用的 GetGeocodeData() 方法使用地理编码 API 返回的数据设置 XDocument 变量的值。
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
ShowGeocodeData() 方法更新必要的 UI 元素的值。
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
在上面的方法中,我利用 LINQ to XML 和 XML 轴属性从 geoDoc 获取所需详细信息。请注意索引轴属性 (0) 的使用。我使用它来获取返回序列中的第一个元素,因为在这种情况下,Google 静态地图 API 只会返回第一个部分匹配的地图图像。以上面 Gigiri, Nairobi 的示例为例,结果将是:

ShowMapImage() 获取并显示返回的 Google 地图图像。
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
通过调用 ZoomIn() 方法可以放大目标地址。
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
ShowMapUsingLatLng() 方法与 ShowMapImage() 类似,不同之处在于前者使用 center 参数和纬度/经度值来设置从静态地图 API 请求的地图中心。当使用箭头按钮滚动地图时,这种方法最为有用。
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
点击向上箭头按钮会调用 MoveUp() 方法。
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
通过 TerrainToggleButton 的 Checked 事件处理程序,将 maptype 从 roadmap 切换到 terrain。
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
要保存当前显示的地图(在当前缩放级别下),将调用 SaveMap() 方法。
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
地图图像将以 PNG 格式保存,尺寸为 500x400。

注意:允许用户保存个人使用的地图是可以的;但是,如果您允许通过电子邮件或社交网络共享图像,则必须通过共享静态地图的 URL 来实现。
致谢
感谢 Google Maps API 产品经理 Thor Mitchell,他为静态地图 API 的服务条款以及静态地图图像的保存提供了富有洞察力的反馈。
同时感谢 Google Geo 团队的 Marc Ridey。
结论
就是这样。希望您从本文中收集到的信息能有所帮助。
历史
- 2011年8月9日:初稿。
- 2011年8月11日:添加了缩放、地图类型和保存功能。
- 2011年8月12日:添加了滚动功能。
- 2011年8月16日:根据 Google Maps 服务条款,添加了在浏览器中打开 Google Maps 的功能。
