一触即发

在 Windows Phone 上安装 Bing Map 磁贴

Charles Petzold

下载代码示例

Charles Petzold在 Windows Phone 中的运动传感器合并从手机的指南针和加速度计创建描述在 3D 空间中手机方向的旋转矩阵的信息。

最近我开始思考如何可以在 Bing 地图结合中使用手机的定位。我预期快速完事糅合,但作业原来是更为复杂。

如果您已经对您的 Windows Phone 试行标准地图程序,你知道显示通常对齐所以这北是手机的顶部。(唯一的例外是当问路到案件地图是面向来指示的方向你旅行的位置,使用地图时)。当然,为北是公约 》 的地图,但在某些情况下,您可能想在手机的地图,所以相对于手机旋转的地图上,北其实北指。

看来如此简单,对不对吗?

地图控制限制

我争取在手机上实现旋转的地图,我与 Bing 地图 Silverlight 控件开始为 Windows Phone,其中中心是一个名为 Microsoft.Phone.Controls.Maps 命名空间中的简单地图控件。

入门的地图控件 (或任何涉及以编程方式访问 Bing 地图),您需要注册在 Bing 地图帐户中心在 bingmapsportal.com。它是直­转发过程来获取凭据密钥,使您的程序访问的 Bing 地图。

Windows Phone 项目中使用的地图控制、 你需要引用 Microsoft.Phone.Controls.Maps 程序集,和您可能要在 XAML 文件 (在一行) 的 XML 命名空间声明:

xmlns:maps="clr-namespace:Microsoft.Phone.Controls.Maps;
  assembly=Microsoft.Phone.Controls.Maps"

实例化基本地图,然后是微不足道的:

<maps:Map CredentialsProvider="credentials-key" />

插入您从 Bing 地图帐户中心获得的实际凭据密钥。

是的在屏幕上获取地图很容易,但当我找出办法来旋转它,我想到了干。 肯定的是,地图类继承的标题属性 MapBase 类,但显然此属性是只为"鸟瞰图"相关地图和地图控件支持只有标准的道路和空中的意见。

当然,这是相当微不足道的地图控件通过给其变换器属性,设置一个 RotateTransform 对象的旋转,但并不是在所有满意的解决方案。 一方面,它往往掩盖版权声明底部的映射,并且这样做,似乎是在违反规定时获取凭据密钥我同意的条件。

我决定放弃地图控制,而是通过访问 Bing 地图 SOAP 服务尝试我的运气在较低水平上。 这一组 Web 服务允许某个程序以获得实际的位图瓷砖构造较大地图的。

访问 SOAP 服务

在生成简单对象访问协议 (SOAP) 周围的 Web 服务,是您的程序和使用 XML 文档的服务器之间传输信息。 这些文档往往涉及相当复杂的数据结构,因此,而不是直接处理 XML,一种更容易的方法是有 Visual Studio 为您创建一个代理类。 这允许您访问与正常 (尽管异步) C# 类和方法调用的 Web 服务的程序。

必应地图 SOAP 服务界面记录在 bit.ly/S3R4lG,和四个单独的服务包括:

  • 地理代码服务:与经度和纬度的地址相匹配。
  • 影像服务:获取地图和瓷砖。
  • 路由服务:获取指示。
  • 搜索服务:找到人和企业。

我只是在影像服务感兴趣。

这篇文章的可下载代码是一个名为 RotatingMapTiles 的单个项目。 通过从项目菜单中选择添加服务引用到此项目添加图像服务的代理。 在添加服务引用对话框中的地址字段中,将复制的文件并按下转到必应地图 SOAP 服务地址节中列出的 URL。 当该服务所在的时候,我给它的 ImageryService Namespace 字段中的名称。

C# 代码生成此服务有一个命名空间,是正常项目的命名空间后, 跟创建服务引用,所以 RotatingMapTile 程序中的 MainPage.xaml.cs 文件包含以下时指定的命名空间使用指令:

using RotatingMapTiles.ImageryService;

影像服务支持两种类型的请求涉及函数调用 GetMapUriAsync 和 GetImageryMetadataAsync。 第一次调用允许您获取特定的位置、 大小和缩放级别,同时在较低级别的第二个工程的静态映射。 这第二个调用返回是一个允许您访问用于汇编的实际位图瓷砖的 URI 模板的元数据之间所有必应地图。

图 1 RotatingMapTiles 中的首页类项目制作两次调用图像 Web 服务以获取元数据为 MapStyle.Road 和 MapStyle.Aerial 的样式显示加载的处理程序。 (MapStyle.Birdseye 也是可用的但是,使用更为复杂。)

图 1 代码来访问 Bing 地图图像 Web 服务

void OnMainPageLoaded(object sender, RoutedEventArgs e)
{
  // Initialize the Bing Maps imagery service
  ImageryServiceClient imageryServiceClient =
    new ImageryServiceClient("BasicHttpBinding_IImageryService");
    imageryServiceClient.GetImageryMetadataCompleted +=
      GetImageryMetadataCompleted;
  // Make a request for the road metadata
  ImageryMetadataRequest request = new ImageryMetadataRequest
  {
    Credentials = new Credentials
    {
      ApplicationId = "credentials-key"
    },
    Style = MapStyle.Road
  };
  imageryServiceClient.GetImageryMetadataAsync(request, "road");
  // Make a request for the aerial metadata
  request.Style = MapStyle.Aerial;
  imageryServiceClient.GetImageryMetadataAsync(request, "aerial");
}

您将需要您自己必应地图凭据密钥,用于替换占位符。

图 2 显示为已完成的异步调用事件处理程序。 信息包括 URI 的 Bing 徽标位图,因此很容易信贷 Bing 地图程序的屏幕上。

图 2 为 Bing 地图 Web 服务已完成处理程序

void GetImageryMetadataCompleted(object sender,
   GetImageryMetadataCompletedEventArgs args)
{
  if (!args.Cancelled && args.Error == null)
  {
    // Get the "powered by" bitmap
    poweredByBitmap.UriSource = args.Result.BrandLogoUri;
    poweredByDisplay.Visibility = Visibility.Visible;
    // Get the range of map levels available
    ImageryMetadataResult result = args.Result.Results[0];
    minimumLevel = result.ZoomRange.From;
    maximumLevel = result.ZoomRange.To;
    // Get the URI and make some substitutions
    string uri = result.ImageUri;
    uri = uri.Replace("{subdomain}", result.ImageUriSubdomains[0]);
    uri = uri.Replace("&token={token}", "");
    uri = uri.Replace("{culture}", "en-US");
    if (args.UserState as string == "road")
      roadUriTemplate = uri;
    else
      aerialUriTemplate = uri;
    if (roadUriTemplate != null && aerialUriTemplate != null)
      RefreshDisplay();
  }
  else
  {
    errorTextBlock.Text =
      "Cannot access Bing Maps: " + args.Error.Message;
  }
}

其他 URI 是您将使用来访问地图拼贴。 有的道路和空中视图,单独 Uri 和 URI 包含标识正是你想要的拼贴的编号的占位符。

地图和瓷砖

必应地图的基础的瓷砖是始终是 256 像素广场的位图。 每个拼贴与特定的经度、 纬度和缩放级别相关联,并包含一个图像拼合使用常见的墨卡托投影在地球表面上的方形区域。

最极端的缩小视图称为级别 1,只有四个瓷砖须涵盖整个世界 — — 或者至少之间积极和消极的 85.05 ° 纬度与世界的一部分 — — 如中所示图 3

The Four Level 1 Tiles
图 3 4 个 1 级瓷砖

我会解释的墙砖上一会儿的数字。 由于瓷砖是 256 像素的正方形,在赤道的每个像素等同于 49 英里左右。

2 级是更精细,和现在 16 瓦片覆盖地球,如中所示图 4

The 16 Level 2 Tiles
图 4 16 2 级瓷砖

中平铺图 4 也是 256 像素平方米,因此在赤道的每个像素是 24 英里左右。 请注意每个平铺在级别 1 4 平铺在级别 2 面积相同。

这项计划将继续:第 3 级有 64 瓷砖、 一级 4 有 256 瓷砖、 以及最多和向上和水平 21 到涵盖地球共超过 4 兆平铺 — — 200 万水平方向和垂直方向的一项决议 (在赤道) 的每个像素的 3 英寸 200 万。

编号拼贴

因为这些数万亿瓦至少几,必须单独希望使用这些程序的引用,他们必须确定明确的、 一贯的方式。 有三个方面 — — 经度、 纬度和缩放级别 — — 和现实的考虑:为尽量减少服务器上的磁盘访问,与同一区域关联的瓷砖应存储彼此靠近,这意味着单一的编号系统,涵盖所有三个尺寸以一些非常聪明的方式。

聪明的编号系统使用的这些地图瓷砖被称为"quadkey"。MSDN 库条,"必应地图平铺系统"由乔舒瓦茨 (bit.ly/SxVojI) 是很好的解释 (包括有用的代码) 的系统,但在这里我会稍有不同的做法。

每个拼贴有独特的 quadkey。 从 Web 服务中获取 Uri 的图块包含占位符字符串"{quadkey}"。 使用 Uri 之一来访问图块之前,您必须将此占位符替换为实际的 quadkey。

图 3图 4 显示缩放级别 1 和 2 quadkeys。前导零是重要的 quadkeys。 (事实上,可能要 quadkey 看作是一个字符串,而不是数字。)在 quadkey 中的位数总是等于拼贴的缩放级别。 在水平 21 瓷砖的识别 21 位数字 quadkeys。

Quadkey 的个别数字总是 0、 1、 2 或 3。 因此,quadkey 其实是一个基地 4 数字。 看看这些二进制文件 (00、 01、 10、 11) 和他们的四个瓷砖的组中的出现的四个数字。 中的每个基地 4 位的第二位真的是水平坐标和第一位是垂直坐标。 位对应经度和纬度,有效地交错在 quadkey 中。

每个级别 1 的瓷砖涵盖同一地区作为一个组级别 2 中的四个拼贴。 可以在级别 1 的瓷砖看作级别 2 的四个"儿童"的"父"。 子平铺的 quadkey 总是以作为其父的相同数字开始,然后添加另一个数字 (0、 1、 2 或 3) 根据其在其父区域内的位置。 孩子从父前往是缩放。 缩放下来是相似的:对于任何儿童 quadkey,您只需通过疲于奔命的最后一位获得父 quadkey。

这里是如何 quadkey 从实际地理经度和纬度。

经度范围从-180 ° 在 Inter­国家日期变更线,然后再次将东去在国际数据行的 180 ° 的增加。 任何经度,首先计算范围从 0 到 1 0.5 代表子午线相对经度:

double relativeLongitude = (180 + longitude) / 360;

现在把固定数量的位数的整数转换的:

int integerLongitude =
  (int)(relativeLongitude * (1 << BITRES));

在我的程序中我已经将 BITRES 设置为 29 21 缩放级别加平铺的像素大小为 8 位。 因此,此整数标识经度精确到最近的像素的平铺在最高的缩放级别。

IntegerLatitude 的计算是因为墨卡托图投影压缩纬度,当你从赤道稍微复杂:

double sinTerm = Math.Sin(Math.PI * latitude / 180);
double relativeLatitude =
  0.5 - Math.Log((1 + sinTerm) / (1 - sinTerm)) 
    / (4 * Math.PI);
int integerLatitude = (int)(relativeLatitude * (1 << BITRES));

IntegerLatitude 范围是从 0 85.05 ° 赤道以北到 85.05 ° 赤道以南的最大价值。

在纽约中央公园的中心有-73.965368 ° 的经度和纬度 40.783271 °。 0.29454 和 0.37572 将会相对值 (到几个小数位数)。 29 位整数经度和纬度值 (二进制文件所示和可读性分组) 如下:

0 1001 0110 1100 1110 0000 1000 0000

0 1100 0000 0101 1110 1011 0000 0000

假设您要显示中级别 12 缩放的中央公园中心瓦。 采取 12 位整数经度和纬度的顶部 (当心 — — 以下数字分组的比 29-位版本略有不同):

0100 1011 0110

0110 0000 0010

这些都是两个二进制数字,但我们需要将其合并以形成基地 4 号。 没有办法做到这一点在代码中使用简单的算术运算符。 你需要一点例程实际经历的各个位,并构造一个长整数或字符串。 为了说明问题,可以只是双纬度的所有位都和把它们当作基地 4 值添加这两个值:

0100 1011 0110

0220 0000 0020

0320 1011 0130

结果就是你需要替换"{quadkey}"占位符的 URI 中为您的 12 位数字 quadkey 获取来自 Web 服务访问的地图拼贴。

图 5 演示例程来构造 quadkey 从截断的整数经度和纬度。 为清楚起见,我已经分成几个部分,创造一个长整型,quadkey 和 quadkey 字符串分隔逻辑。

图 5 的例程,以计算 Quadkey

string ToQuadKey(int longitude, int latitude, int level)
{
  long quadkey = 0;
  int mask = 1 << (level - 1);
  for (int i = 0; i < level; i++)
  {
    quadkey <<= 2;
    if ((longitude & mask) != 0)
      quadkey |= 1;
    if ((latitude & mask) != 0)
      quadkey |= 2;
    mask >>= 1;
  }
  strBuilder.Clear();
  for (int i = 0; i < level; i++)
  {
    strBuilder.Insert(0, (quadkey & 3).ToString());
    quadkey >>= 2;
  }
  return strBuilder.ToString();
}

图 6 显示的道路和空中瓦片的这个 quadkey。 中央公园中心实际上是向下,这些图像的底部有点左侧的中心。 这是可预测从整数经度和纬度。 看后前 12 位的整数经度的下一个 8 位:Bits 是 0111年 0000 或 112。 纬度的下一个 8 位是 1111年 0101年或 245。 这意味着中央公园中心是从左侧第 112 像素和这些瓷砖下来的 245 像素。

The Tiles for Quadkey “032010110130”
图 6 瓦片的 Quadkey"032010110130"

平铺瓷砖

一旦你已经被截断整数经度和纬度的一定数量的到特定的缩放级别对应的位,获得相邻瓷砖是管理单元:只是递增和递减的经度和纬度整数和窗体新 quadkeys。

你已经看到了从 RotatingMapTiles 项目的首页类三种方法。 该程序将使用 GeoCoordinateWatcher 来获取的经度和纬度的电话,并将坐标转换为整数值,如上文所示。 应用程序栏有三个按钮:道路和空中的视图之间切换并增加和减小缩放级别。

该程序具有除按钮外的没有其他触摸界面。 它总是显示在屏幕的中心 GeoCoordinateWatcher 索取的位置和构造与 25 5 x 5 阵列,总是甚至用旋转填充 480 x 800 像素的屏幕,配置中的图像元素的总地图。 首页类在其构造函数中创建这些 25 图像元素和 BitmapImage 对象。

每当 GeoCoordinateWatcher 与一个新的位置,或缩放级别或地图样式更改,在 RefreshDisplay 方法上来图 7 调用。 此方法显示如何获取新的 Uri 和简单地设置为现有的 BitmapImage 对象。

图 7 中 RotatingMapTiles 的 RefreshDisplay 方法

void RefreshDisplay()
{
  if (roadUriTemplate == null || aerialUriTemplate == null)
    return;
  if (integerLongitude == -1 || integerLatitude == -1)
    return;
  // Get coordinates and pixel offsets based on current zoom level
  int croppedLongitude = integerLongitude >> BITRES - zoomLevel;
  int croppedLatitude = integerLatitude >> BITRES - zoomLevel;
  int xPixelOffset = (integerLongitude >> BITRES - zoomLevel - 8) % 256;
  int yPixelOffset = (integerLatitude >> BITRES - zoomLevel - 8) % 256;
  // Prepare for the loop
  string uriTemplate = mapStyle ==
    MapStyle.Road ?
roadUriTemplate : aerialUriTemplate;
  int index = 0;
  int maxValue = (1 << zoomLevel) - 1;
  // Loop through the 5x5 array of Image elements
  for (int row = -2; row <= 2; row++)
    for (int col = -2; col <= 2; col++)
    {
      // Get the Image and BitmapImage
      Image image = imageCanvas.Children[index] as Image;
      BitmapImage bitmap = image.Source as BitmapImage;
      index++;
      // Check if you've gone beyond the bounds
      if (croppedLongitude + col < 0 ||
        croppedLongitude + col > maxValue ||
        croppedLatitude + row < 0 ||
        croppedLatitude + row > maxValue)
      {
        bitmap.UriSource = null;
      }
      else
      {
        // Calculate a quadkey and set URI to bitmap
        int longitude = croppedLongitude + col;
        int latitude = croppedLatitude + row;
        string strQuadkey =
          ToQuadKey(longitude, latitude, zoomLevel);
        string uri = uriTemplate.Replace("{quadkey}", strQuadkey);
        bitmap.UriSource = new Uri(uri);
      }
      // Position the Image element
      Canvas.SetLeft(image, col * 256 - xPixelOffset);
      Canvas.SetTop(image, row * 256 - yPixelOffset);
    }
}

要保留此程序相当简单,它不会试图掩饰意见和缩放级别之间的转换。 往往在整个屏幕就空白正在加载新砖。

但该程序不会旋转地图。 旋转的逻辑基于运动传感器和 RotateTransform,非常独立的程序的其余部分。 图 8 布鲁克林大桥跨散步显示我考虑我的 Windows Phone (或也许 Windows Phone 仿真程序)。 顶部的电话是指出方向我行走,和左上角的小箭头指示北。

The RotatingMapTiles Display
图 8 RotatingMapTiles 显示

查尔斯 · Petzold 是 MSDN 杂志和"编程 Windows,第 6 版"的作者长期贡献 (O'Reilly 媒体,2012年),有关编写应用程序的 Windows 8 的一本书。 他的网站是 charlespetzold.com

衷心感谢以下技术专家对本文的审阅:托马斯 · Petchel