本文章是由機器翻譯。

觸控與執行

組合 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