Este artigo foi traduzido por máquina.

Toque e ouça

Montando peças de mapa do Bing no Windows Phone

Charles Petzold

Baixar o código de exemplo

O sensor de movimento em Windows Phone consolida informações a partir do telefone bússola e acelerômetro para criar uma matriz de rotação que descreve a orientação do telefone no espaço 3D.

Recentemente comecei pensando como orientação do telefone poderia ser usada em combinação com o Bing Maps. Eu esperava um mashup mais rápido, mas o trabalho acabou por ser um pouco mais complexo.

Se você já experimentou com o programa padrão de mapas em seu Windows Phone, você sabe que a exibição geralmente é alinhada para que o norte é para cima do telefone. (A única exceção é quando você estiver usando o mapa para saber como chegar em um local, em que caso o mapa é orientado para indicar a direção que você está viajando). Para o norte é a Convenção para os mapas, é claro, mas em alguns casos que você pode querer mapa do telefone para girar em relação ao telefone assim que o Norte no mapa é realmente apontando norte.

Parece tão simples, né?

Limitações do controle de mapa

Em minha busca para implementar um mapa girando no telefone, eu comecei com o Bing Maps Silverlight Control para Windows Phone, o centro do que é um controle chamado simplesmente de mapa no Maps namespace.

Para começar com o controle do mapa (ou qualquer coisa que envolve acessar programaticamente o Bing Maps) você precisa se registrar no centro de conta do Bing Maps no bingmapsportal.com. É uma linha reta­encaminhar o processo para obter uma chave de credenciais que fornece seu programa acesso ao Bing Maps.

No projeto Windows Phone, que usa o controle do mapa, você vai precisar de uma referência ao assembly Maps e você provavelmente vai querer uma declaração de namespace XML no arquivo XAML (em uma linha):

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

Instanciar um mapa básico é trivial:

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

Insira a chave de credenciais reais que você obteve do Bing Maps conta centro.

Sim, obtendo um mapa na tela é fácil, mas quando eu procurei maneiras de girá-lo, eu vim seco. Com certeza, a classe de mapa herda uma propriedade de título da classe MapBase, mas aparentemente, essa propriedade é relevante apenas para o "olho de pássaro" mapas e o controle de mapa oferece suporte apenas a estrada padrão e vistas aéreas.

Claro, é bastante trivial para girar o controle de mapa definindo um objeto RotateTransform para sua propriedade RenderTransform, mas eu não estava feliz com essa solução. Por um lado, tendem a obscurecer o aviso de direitos autorais na parte inferior do mapa e fazendo que parecia estar em violação das condições que eu concordei quando a obtenção de uma chave de credenciais.

Decidi abandonar o controle de mapas e em vez disso tente minha sorte em um nível muito inferior ao acessar os serviços do Bing Maps SOAP. Este conjunto de serviços da Web permite que um programa obter as peças reais bitmap do qual são construídos mapas maiores.

Acessando o serviço SOAP

Em um serviço Web construído em torno de SOAP Simple Object Access Protocol (), a informação é transferida entre seu programa e o servidor utilizando documentos XML. Muitas vezes esses documentos envolvem estruturas de dados bastante complexo, assim em vez de manipulação XML diretamente, uma abordagem muito mais fácil é ter o Visual Studio criar uma classe de proxy para você. Isso permite que o seu programa acessar o serviço Web com classes c# normais (embora assíncrono) e chamadas de método.

A interface de serviços SOAP de mapas do Bing está documentada em bit.ly/S3R4lGe é composto por quatro serviços distintos:

  • Serviço Geocode: Jogo de endereços com longitude e latitude.
  • Serviço de imagens: Obter mapas e telhas.
  • Serviço de rota: Obter direcções.
  • Serviço de pesquisa: Localize pessoas e empresas.

Eu só estava interessado no serviço de imagens.

O código de download para este artigo é um projeto único, chamado RotatingMapTiles. Eu adicionei um proxy para o serviço de imagens para este projeto escolhendo Add Service Reference no menu de Project. No campo de endereço da caixa de diálogo Adicionar referência de serviço, copiei o URL listado na seção endereços de serviços do Bing Maps SOAP da documentação e Go pressionado. Quando o serviço foi localizado, eu dei-lhe um nome de ImageryService no campo Namespace.

O código c# gerado para este serviço tem um namespace que é o namespace do projeto normal, seguido pelo namespace que você especificar ao criar a referência de serviço, então o arquivo MainPage.xaml.cs no programa RotatingMapTile contém o seguinte usando diretiva:

using RotatingMapTiles.ImageryService;

O serviço de imagens suporta dois tipos de solicitações que envolvem as chamadas de função GetMapUriAsync e GetImageryMetadataAsync. A primeira chamada permite que você obtenha mapas estáticos de um determinado nível de zoom, tamanho e localização, enquanto o segundo funciona em um nível inferior. Entre os metadados que esta segunda chamada retorna é um modelo URI que permite que você acesse as telhas bitmap reais que são usadas para montar todo o Bing maps.

Figura 1 mostra o manipulador de carregado para a classe MainPage o RotatingMapTiles projecto faz duas chamadas para o serviço Web de imagens para obter metadados para os estilos MapStyle.Road e MapStyle.Aerial. (MapStyle.Birdseye também está disponível, mas é um pouco mais complexo de usar).

Figura 1 código para acessar o serviço de Web de imagens do Bing Maps

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");
}

Você terá sua própria chave de credencial do Bing Maps para substituir o espaço reservado.

Figura 2 mostra o manipulador para o evento concluído da chamada assíncrona. A informação inclui um URI para um bitmap com o logotipo do Bing, por isso é fácil de crédito Bing mapas na tela do programa.

Figura 2 o manipulador concluído para o serviço de Web de mapas do Bing

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

O outro URI é o que você vai usar para acessar as mapa de telhas. Existem URIs separado para a estrada e vistas aéreas e o URI contém um espaço reservado para um número que identifica precisamente o azulejo que você quer.

Mapas e telhas

Os azulejos que formam a base de mapas do Bing são bitmaps que são sempre Praça de 256 pixels. Cada telha é associada com um determinado nível de zoom, a latitude e a longitude e contém uma imagem de uma área quadrada na superfície da terra achatada usando a projeção de Mercator comum.

O modo de exibição de zoom-out mais extremo é conhecido como nível 1, e apenas quatro telhas são necessárias para cobrir todo o mundo — ou pelo menos a parte do mundo com latitudes entre positivo e negativo 85.05 ° — conforme Figura 3.


Figura 3 as quatro peças de nível 1

Vou explicar os números sobre as telhas em um momento. Porque as telhas são 256 quadrado de pixels, no Equador, cada pixel é equivalente a cerca de 49 km.

Nível 2 é mais granular e agora 16 telhas cobrem a terra, como mostrado na Figura 4.


Figura 4 as 16 telhas de nível 2

As telhas em Figura 4 também são 256 pixels quadrados, então no Equador, cada pixel é cerca de 24 km. Observe que cada telha em nível 1 cobre a mesma área como 4 peças de nível 2.

Este regime continua: Nível 3 tem 64 telhas, nível 4 tem 256 telhas e até e acima e até o nível 21, que cobre a terra com um total de mais de 4 trilhões telhas — 2 milhões na horizontal e 2 milhões verticalmente de resolução (no Equador) de 3 polegadas por pixel.

Numeração das telhas

Porque pelo menos alguns destes trilhões de telhas devem ser referenciado individualmente por um programa que pretende usá-los, eles devem ser identificados de forma clara e consistente. Há três dimensões envolvidas — nível de zoom, a latitude e a longitude e uma consideração prática bem: Para minimizar os acessos ao disco no servidor, associadas com a mesma área de telhas devem ser armazenadas perto uns dos outros, o que implica um sistema de numeração único que engloba todas as três dimensões de maneira muito inteligente.

O inteligente sistema de numeração para estas mapeiam telhas é chamado um "quadkey". Artigo da biblioteca MSDN, "Bing Maps telha sistema" por Joe Schwartz (bit.ly/SxVojI) é uma explicação muito boa do sistema (incluindo código útil), mas vou dar aqui uma abordagem um pouco diferente.

Cada telha tem um exclusivo quadkey. A telha URIs obtidas do serviço da Web contém uma seqüência de caracteres de espaço reservado "{quadkey}". Antes de usar um dos URIs para acessar uma telha, você deve substituir esse espaço reservado com um quadkey real.

Figura 3 e Figura 4 mostrar o quadkeys para zoom níveis 1 e 2.  Zeros à esquerda são importantes quadkeys. (Na verdade, convém pensar a quadkey como uma seqüência de caracteres em vez de um número.) O número de dígitos em um quadkey é sempre igual ao nível de zoom da telha. As telhas no nível 21 são identificadas com 21 dígitos quadkeys.

Os dígitos individuais de um quadkey são sempre 0, 1, 2 ou 3. Assim, o quadkey é realmente um número base-4. Olhe para estes quatro dígitos binários (00, 01, 10, 11) e como eles aparecem em um grupo de quatro peças. Em cada base de 4 dígitos, o segundo bit é realmente uma coordenada horizontal e o primeiro bit é uma coordenada vertical. Os bits correspondem a uma longitude e latitude, que efetivamente são intercalados no quadkey.

Cada telha em nível 1 cobre a mesma área como um grupo de quatro telhas no nível 2. Você pode pensar de uma telha em nível 1 como um "pai" de quatro "filhos" de nível 2. Quadkey de uma telha de criança sempre começa com os mesmos dígitos como seu pai e, em seguida, adiciona outro dígito (0, 1, 2 ou 3), dependendo de sua localização dentro da área de seu pai. Passando de pai para filho é um zoom. Zoom para baixo é semelhante: Para qualquer criança quadkey, você pode obter o quadkey pai simplesmente por decepar o último dígito.

Aqui está como derivar uma quadkey de uma latitude e longitude geográfica real.

Varia de longitude de-180 ° no Inter­linha de data nacional e então aumenta vai novamente Oriente 180 ° na linha de dados internacional. Para qualquer longitude, primeiro calcule um que varia de 0 a 1, com 0,5 representando o meridiano de longitude relativa:

double relativeLongitude = (180 + longitude) / 360;

Agora que converta para um número inteiro de um número fixo de bits:

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

No meu programa eu jogo BITRES 29 para os níveis de zoom 21 além de 8 bits para o tamanho do pixel da telha. Assim, esse número inteiro identifica uma longitude precisa para o pixel mais próximo de um azulejo ao mais alto nível de zoom.

O cálculo da integerLatitude é um pouco mais complexo porque a projeção de Mercator mapa comprime latitudes como você chegar mais longe do Equador:

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

O integerLatitude varia de 0 a 85.05 ° ao norte do Equador para o valor máximo a 85.05 ° ao sul do Equador.

O centro do Central Park em Nova York tem uma longitude de-73.965368 º e uma latitude de 40.783271 º. Os valores relativos são (com poucas casas decimais), 0.29454 e 0.37572. Os 29 bits latitude e longitude valores inteiros (mostrado em binário e agrupados para facilitar a leitura mais fácil) são:

0 1001 0110 1100 1110 0000 1000 0000

0 1100 0000 0101 1110 1011 0000 0000

Suponha que você queira uma peça que mostra o centro de Central Park em um zoom de nível 12. Tirar o top 12 bits das latitudes e longitudes inteiro (cuidado — os dígitos seguintes são agrupados um pouco diferente do que as versões 29 bits):

0100 1011 0110

0110 0000 0010

Estes são dois números binários, mas precisamos combiná-los para formar um número base-4. Não há nenhuma maneira de fazer isso no código usando operadores aritméticos simples. Você precisa de um pouco de rotina que realmente atravessa os bits individuais e constrói um inteiro longo ou uma seqüência de caracteres. Para fins ilustrativos, você pode simplesmente dobrar todos os bits em latitude e adicionar dois valores como se fossem valores base-4:

0100 1011 0110

0220 0000 0020

0320 1011 0130

O resultado é a quadkey de 12 dígitos, você vai precisar para substituir o espaço reservado para "{quadkey}" no URI você obter do serviço da Web para acessar a telha do mapa.

Figura 5 mostra uma rotina para construir um quadkey das latitudes e longitudes inteiro truncado. Para maior clareza, já separei a lógica em seções que geram um inteiro longo quadkey e uma seqüência de quadkey.

Figura 5 rotina para calcular um 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();
}

Figura 6 mostra a rodoviária e aéreas telhas para este quadkey. O centro de Central Park é na verdade a maneira para baixo na parte inferior dessas imagens, um pouco à esquerda do centro. Isso é previsível das latitudes e longitudes inteiro. Olhe para os próximos 8 bits da longitude inteiro após os primeiros 12 bits: Os bits são 0111 0000 ou 112. Os próximos 8 bits da latitude são 1111 0101 ou 245. Isso significa que o centro de Central Park é o pixel 112a da esquerda e o número 254 pixel para baixo os azulejos.


Figura 6 as telhas para Quadkey "032010110130"

Telha telhas

Uma vez que você já truncado um inteiro longitude e latitude para um certo número de bits correspondente a um nível de zoom especial, obter telhas adjacentes é moleza: Simplesmente incrementar e decrementar inteiros de longitude e latitude e quadkeys de nova forma.

Você já viu três métodos da classe MainPage do projeto RotatingMapTiles. O programa usa um GeoCoordinateWatchere para obter a latitude e longitude do telefone e converte as coordenadas para valores inteiros, conforme mostrado anteriormente. A barra de aplicativo tem três botões: para alternar entre a estrada e vistas aéreas e para aumentar e diminuir o nível de zoom.

O programa não tem nenhuma interface de toque outro além de botões. Ele sempre exibe a localização Obtida de GeoCoordinateWatcher no centro da tela e constrói o mapa total com 25 elementos de imagem em uma matriz de 5 x 5, uma configuração que sempre enche a tela de 480 x 800 pixels, mesmo com a rotação. A classe MainPage cria estes 25 elementos de imagem e BitmapImage objetos em seu construtor.

Sempre que o GeoCoordinateWatcher vem com um novo local, ou as alterações de estilo de nível ou mapa de zoom, o método de RefreshDisplay em Figura 7 é chamado. Este método mostra como o novo URIs são obtidos e simplesmente definido como objetos de BitmapImage existentes.

Figura 7 o método de RefreshDisplay em RotatingMapTiles

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

Para manter este programa razoavelmente simples, não tente alisar sobre as transições entre níveis de zoom e vistas. Muitas vezes, toda a tela fica em branco como telhas novas estão sendo carregadas.

Mas o programa rodar o mapa. A lógica de rotação é baseada no sensor de movimento e um RotateTransform e é praticamente independente do resto do programa. Figura 8 mostra-me tomar meu Windows Phone (ou talvez o emulador Windows Phone) para um passeio através da ponte de Brooklyn. A parte superior do telefone é apontada na direção que eu estou andando, e a pequena seta no canto superior esquerdo indica o norte.


Figura 8 Display RotatingMapTiles

Charles Petzold é um colaborador de longa data de MSDN Magazine e autor de "Programação Windows, 6ª edição" (o ' Reilly Media, 2012), um livro sobre como escrever aplicativos para Windows 8. Seu site é charlespetzold.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Thomas Petchel