Bing-карты в WinRT

120

Класс Geolocator не относится к категории классов датчиков и находится в совершенно другом пространстве имен: Windows.Devices.Geolocation. Тем не менее он работает по похожему принципу: вы приказываете ему начать работу, а он сообщает вам, когда компьютер изменит свое географическое местонахождение и в какой точке он оказался.

О том, что вашему приложению необходимы данные позиционирования, необходимо явно указать в разделе Capabilities файла Package.appxmanifest. После этого Windows 8 запрашивает подтверждение у пользователя при первом запуске программы.

Как правило, данные Geolocator используются в сочетании с картами. В Windows 8 нет встроенного элемента управления для карт Bing, но вы можете загрузить инструментарий, который позволит добавить этот элемент в приложение. Для этого вам понадобится регистрационный ключ, который можно получить на сайте www.bingmapsportal.com.

Но в следующей программе я пойду по иному пути. На экране будет отображаться карта, поворачивающаяся в зависимости от ориентации планшета. Поворот позволяет совместить север карты с фактическим севером (или тем направлением, которое планшет считает северным). Вместо элемента управления для карт Bing я использую SOAP-службу карт Bing для загрузки отдельных плиток и их объединения в карту. При этом вам также понадобится регистрационный ключ.

Когда вы запустите программу RotatingMap, у вас появится естественное желание использовать пальцы для прокрутки и масштабирования карты. Ничего не выйдет. У программы нет сенсорного интерфейса! Для простоты программа просто располагает карту так, чтобы текущая позиция находилась в центре, и изменяет ориентацию карты при изменении позиции. В строке приложения имеются кнопки для увеличения и уменьшения масштаба, а также для переключения режима просмотра, но это все.

Ниже приведен файл XAML. Все плитки, образующие карту, размещаются на панели Canvas с именем imageCanvas. Обратите внимание на преобразование RotateTransform для поворота Canvas относительно центра:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Canvas Name="imageCanvas"
                HorizontalAlignment="Center"
                VerticalAlignment="Center">
            <Canvas.RenderTransform>
                <RotateTransform x:Name="imageCanvasRotate" />
            </Canvas.RenderTransform>
        </Canvas>

        <!-- Круг, обозначающий текущую позицию -->
        <Ellipse Name="locationDisplay"
                 HorizontalAlignment="Center"
                 VerticalAlignment="Center"
                 Width="24"
                 Height="24"
                 Stroke="Red"
                 StrokeThickness="6"
                 Visibility="Collapsed" />

        <!-- Стрелка для обозначения севера -->
        <Border Margin="12"
                Background="Black"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Width="36"
                Height="36"
                CornerRadius="18">
            <Path Stroke="White"
                  StrokeThickness="3"
                  Data="M 18 4 L 18 24 M 12 12 L 18 4 24 12">
                <Path.RenderTransform>
                    <RotateTransform x:Name="northArrowRotate"
                                     CenterX="18" CenterY="18" />
                </Path.RenderTransform>
            </Path>
        </Border>

        <!-- Метка "powered by bing" -->
        <Border Background="Black"
                HorizontalAlignment="Center"
                VerticalAlignment="Bottom"
                Margin="12"
                CornerRadius="12"
                Padding="3">

            <StackPanel Name="poweredByDisplay"
                        Orientation="Horizontal"
                        Visibility="Collapsed">
                <TextBlock Text=" powered by "
                           Foreground="White"
                           VerticalAlignment="Center" />
                <Image Stretch="None">
                    <Image.Source>
                        <BitmapImage x:Name="poweredByBitmap" />
                    </Image.Source>
                </Image>
            </StackPanel>
        </Border>
    </Grid>

    <Page.BottomAppBar>
        <AppBar Name="bottomAppBar"
                IsEnabled="False">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
                
                <AppBarToggleButton Name="streetViewAppBarButton"
                                    Icon="Street"
                                    Checked="OnStreetViewAppBarButtonChecked"
                                    Unchecked="OnStreetViewAppBarButtonChecked" />

                <AppBarButton Name="zoomInAppBarButton"
                              Icon="Zoom"
                              Click="OnZoomInAppBarButtonClick" />

                <AppBarButton Name="zoomOutAppBarButton"
                              Icon="ZoomOut"
                              Click="OnZoomOutAppBarButtonClick" />
            </StackPanel>
        </AppBar>
    </Page.BottomAppBar>
</Page>

SOAP-службу карт Bing можно использовать «вручную», передавая туда-сюда громоздкие файлы XML, но гораздо удобнее работать с веб-службой через класс-посредник, сгенерированный Visual Studio. В этом случае веб-служба с точки зрения программы представляет собой набор структур, перечислений и асинхронных вызовов. Чтобы сгенерировать посредника для программы RotatingMap, щелкните правой кнопкой мыши на имени проекта в окне Solution Explorer и выберите в меню команду Add Service Reference. Когда диалоговое окно запросит адрес, вставьте URL-адрес Imagery Service (опубликованный на странице "Bing Maps SOAP Services Addresses" с URL-адресами трех других веб-служб, связанных с картами Bing). Так как я указал имя ImageryService, Visual Studio генерирует код с использованием пространства имен RotatingMap.ImageryService.

Служба поддерживает два типа запросов: GetMapUriAsync и GetImageryMetadataAsync. Запрос первого типа позволяет получить статическую карту конкретного места. Тем не менее я выбрал второй тип, который возвращает информацию для загрузки отдельных «плиток», из которых затем собирается полная карта.

Начнем с кода RotatingMap в конструкторе MainPage. В конфигурации приложения сохраняются только два значения: стиль карты (значение из перечисления MapStyle, сгенерированное в составе кода веб-службы; обозначает режим просмотра — схема или спутниковый) и целочисленный уровень увеличения карты:

using System;
using Windows.UI.Xaml.Controls;
using Windows.Devices.Sensors;
using Windows.UI.Core;
using Windows.Graphics.Display;
using Windows.UI.Xaml;
using Windows.Devices.Geolocation;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Media;
using System.Text;
using Windows.Foundation.Collections;
using Windows.Storage;
using System.Threading.Tasks;

using WinRTTestApp.ImageryService;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        // ...

        // Сохраняется в параметрах приложения
        MapStyle mapStyle = MapStyle.Aerial;
        int zoomLevel = 12;

        public MainPage()
        {
            this.InitializeComponent();
            DisplayProperties.AutoRotationPreferences = DisplayProperties.NativeOrientation;
            Loaded += OnMainPageLoaded;
            SizeChanged += OnMainPageSizeChanged;

            // Получение параметров конфигурации с последующим сохранением
            IPropertySet propertySet = ApplicationData.Current.LocalSettings.Values;

            if (propertySet.ContainsKey("ZoomLevel"))
                zoomLevel = (int)propertySet["ZoomLevel"];

            if (propertySet.ContainsKey("MapStyle"))
                mapStyle = (MapStyle)(int)propertySet["MapStyle"];

            Application.Current.Suspending += (sender, args) =>
            {
                propertySet["ZoomLevel"] = zoomLevel;
                propertySet["MapStyle"] = (int)mapStyle;
            };
        }
        
        // ...
    }
}

Все обращения к веб-службе выполняются исключительно в обработчике Loaded. Необходимо сделать два вызова: для получения метаданных карт для дорожного и спутникового режима. Информация сохраняется в двух экземплярах локального класса с именем ViewParams. Важнейшей частью метаданных является шаблон URI для загрузки отдельных плиток карты. Класс ViewParams также содержит поля для минимального и максимального уровня увеличения, но я знаю, что уровень увеличения изменяется в диапазоне от 1 до 21. Вы увидите, что в других частях кода также предполагается, что верхняя граница уровня увеличения равна 21:

public sealed partial class MainPage : Page
{
    // ...

    // Хранение параметров двух режимов
    class ViewParams
    {
        public string UriTemplate;
        public int MinimumLevel;
        public int MaximumLevel;
    }
    ViewParams aerialParams;
    ViewParams roadParams;

    Geolocator geolocator = new Geolocator();
    Inclinometer inclinometer = Inclinometer.GetDefault();

    // ...

    private async void OnMainPageLoaded(object sender, RoutedEventArgs e)
    {
        // Инициализация картографического сервиса Bing
        ImageryServiceClient imageryServiceClient =
        new ImageryServiceClient(
            ImageryServiceClient.EndpointConfiguration.BasicHttpBinding_IImageryService);

        // Два запроса: для дорожного и спутникового режимов
        ImageryMetadataRequest request = new ImageryMetadataRequest
        {
        Credentials = new Credentials
        {
            ApplicationId = "здесь вставьте свой ключ"
        },
        Style = MapStyle.Road
        };

        Task<ImageryMetadataResponse> roadStyleTask =
            imageryServiceClient.GetImageryMetadataAsync(request);

        request = new ImageryMetadataRequest
        {
                Credentials = new Credentials
                {
                    ApplicationId = "здесь вставьте свой ключ"
                },
                Style = MapStyle.Aerial
        };

        Task<ImageryMetadataResponse> aerialStyleTask =
            imageryServiceClient.GetImageryMetadataAsync(request);

        // Ожидание завершения обеих задач
        Task.WaitAll(roadStyleTask, aerialStyleTask);

        // Проверка возможных ошибок
        if (!roadStyleTask.IsCanceled && !roadStyleTask.IsFaulted &&
            !aerialStyleTask.IsCanceled && !aerialStyleTask.IsCanceled)
        {
            // Получение метки "powered by"
            poweredByBitmap.UriSource = roadStyleTask.Result.BrandLogoUri;
            poweredByDisplay.Visibility = Visibility.Visible;

            // Получение URI и максимального/минимального уровня увеличения
            roadParams = CreateViewParams(roadStyleTask.Result.Results[0]);
            aerialParams = CreateViewParams(aerialStyleTask.Result.Results[0]);

            // Получение текущего места
            Geoposition geoPosition = await geolocator.GetGeopositionAsync();
            GetLongitudeAndLatitude(geoPosition.Coordinate);
            RefreshDisplay();

            // Получении обновлений об изменении места
            geolocator.PositionChanged += OnGeolocatorPositionChanged;

            // Включение строки приложения
            bottomAppBar.IsEnabled = true;
            streetViewAppBarButton.IsChecked = mapStyle == MapStyle.Road;

            // Получение текущего отклонения
            if (inclinometer != null)
            {
                SetRotation(inclinometer.GetCurrentReading());
                inclinometer.ReadingChanged += OnInclinometerReadingChanged;
            }
        }
    }

    private ViewParams CreateViewParams(ImageryMetadataResult result)
    {
        string uri = result.ImageUri;
        uri = uri.Replace("{subdomain}", result.ImageUriSubdomains[0]);
        uri = uri.Replace("&token={token}", "");
        uri = uri.Replace("{culture}", "en-us");

        return new ViewParams
        {
        UriTemplate = uri,
        MinimumLevel = result.ZoomRange.From,
        MaximumLevel = result.ZoomRange.To
        };
    }
        
    // ...
}

Загрузка метаданных двух режимов осуществляется двумя асинхронными вызовами, которые не зависят друг от друга, а следовательно, могут выполняться одновременно. Эта ситуация идеально подходит для применения метода Task.WaitAll, ожидающего завершения нескольких объектов Task.

Когда оба обращения к веб-службе завершатся успехом, программа запускает классы Geolocator и Inclinometer. Класс Inclinometer используется исключительно для получения отклонения, по которому осуществляется поворот карты и стрелки, обозначающей север:

public sealed partial class MainPage : Page
{
    // ...
    private async void OnInclinometerReadingChanged(Inclinometer sender,
                        InclinometerReadingChangedEventArgs e)
    {
        await this.Dispatcher.RunAsync(
        CoreDispatcherPriority.Normal, () =>
        {
            SetRotation(e.Reading);
        });
    }

    private void SetRotation(InclinometerReading inclinometerReading)
    {
        if (inclinometerReading == null)
            return;

        imageCanvasRotate.Angle = inclinometerReading.YawDegrees;
        northArrowRotate.Angle = inclinometerReading.YawDegrees;
    }

    // ...
}

После завершения обработчика Loaded у программы появляются два шаблона URI, которые могут использоваться для загрузки отдельных плиток карты. Плитки, из которых складываются карты Bing, представляют собой квадратные растровые изображения со стороной 256 пикселов. Каждая плитка связывается с определенной широтой, долготой и уровнем увеличения и содержит часть карты мира в распространенной проекции Меркатора.

На уровне увеличения 1 вся Земля (а точнее, часть земной поверхности в области между 85,05° северной и южной широты) накрывается четырьмя плитками.

К числам на плитках мы еще вернемся. Каждая плитка представляет собой квадрат со стороной 256 пикселов, так что на каждый пиксел приходится около 49 миль:

Карта Bing в масштабе 1

На уровне увеличения 2 вся Земля накрывается 16 плитками:

Карта Bing в масштабе 2

Эти плитки тоже представляют собой квадраты со стороной 256 пикселов, так что на экваторе каждый пиксел соответствует примерно 24 милям.

Каждая плитка с уровнем увеличения 1 покрывает такую же область, как четыре плитки с уровнем увеличения 2. Разбиение продолжается по тому же принципу: с уровнем увеличения 3 карта состоит из 64 плиток, с уровнем увеличения 4 — из 256 плиток и так далее вплоть до уровня 21, на котором (по крайней мере теоретически) Земля накрывается более чем 4 триллионами плиток - 2 миллиона но горизонтали и 2 миллиона по вертикали, с разрешением до 3 дюймов на пиксел на экваторе.

Как разумно организовать хранение такого количества плиток? Следует учесть, что плитка идентифицируется тремя показателями — широтой, долготой и уровнем увеличения, а для максимальной эффективности в предоставлении данных плиток через веб-службу плитки, накрывающие одну область, должны хранится на серверах поблизости друг от друга.

Здесь необходима тщательно продуманная схема нумерации. Каждой плитке назначается уникальный квадроключ (quadkey). Шаблоны URI, полученные от веб-службы карт Bing, содержат «{quadkey}», который заполняется ссылкой на реальную плитку. На двух приведенных схемах квадроключи конкретных плиток указываются в левом верхнем углу. Начальные нули важны! Количество цифр в квадроключей равно уровню увеличения, то есть плитки с уровнем увеличения 21 идентифицируются квадроключами, состоящими из 21 цифры.

Квадроключ состоит только из цифр 0,1,2 и 3, то есть квадроключи в действительности представляют собой числа, записанные в системе счисления с основанием 4. В двоичной системе счисления цифры 0,1,2 и 3 представляются в виде 00,01,10 и 11. Первый бит определяет вертикальную, а второй — горизонтальную координату. Таким образом, биты соответствуют «сплетению» долготы и широты.

Как вы уже видели, каждая плитка с уровнем увеличения 1 соответствует четырем плиткам с уровнем увеличения 2; эти плитки можно рассматривать в контексте отношений «родитель/потомок». Квадроключ потомка всегда начинается с тех же цифр, что и квадроключ родителя, но к ним добавляется еще одна цифра в зависимости от местонахождения потомка относительно родителя. Квадроключ родителя можно определить по квадроключу потомка простым отсечением последней цифры.

Чтобы использовать веб-службу карт Bing, необходимо вычислить квадроключ по широте и долготе. Код приведенного ниже метода GetLongitudeAndLatitude демонстрирует первый шаг — преобразование широты и долготы от Geolocator в относительные значения типа double в диапазоне от 0 до 1, а затем в целые значения:

public sealed partial class MainPage : Page
{
    const int BITRES = 29;
    int integerLongitude = -1;
    int integerLatitude = -1;
    
    // ...
    
    private async void OnGeolocatorPositionChanged(Geolocator sender, 
            PositionChangedEventArgs e)
    {
        await this.Dispatcher.RunAsync(
            CoreDispatcherPriority.Normal, () =>
        {
            GetLongitudeAndLatitude(e.Position.Coordinate);
            RefreshDisplay();
        });
    }

    private void GetLongitudeAndLatitude(Geocoordinate geoCoordinate)
    {
        locationDisplay.Visibility = Visibility.Visible;

        // Вычисление целых значений широты и долготы
        double relativeLongitude = (180 + geoCoordinate.Longitude) / 360;
        integerLongitude = (int)(relativeLongitude * (1 << BITRES));

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

}

Значение BITRES, равное 29, складывается из 21 бита в квадроключе уровня увеличения 21 и 8 битов размера плитки; таким образом, эти целые значения определяют широту и долготу с точностью до ближайшего пиксела плитки с максимальным увеличением. Вычисление integerLongitude тривиально, но integerLatitude вычисляется более сложно, потому что в проекции Меркатора происходит сжатие широт при удалении от экватора.

Пример: центр Центрального парка в Нью-Йорке имеет долготу -73,965368 и широту 40,783271. Относительные значения double (до нескольких разрядов) равны 0,29454 и 0,37572. 29-разрядные целые значения (приведенные в двоичном виде и сгруппированные по четыре разряда для удобства чтения) выглядят так:

0 1001 0110 1100 1110 0000 1000 0000
0 1100 0000 0101 1110 1011 0000 0000

Предположим, нам нужна широта и долгота на уровне увеличения 12. Для этого следует отделить старшие 12 разрядов целых значений долготы и широты. (Будьте внимательны! Группировка цифр при этом изменяется.)

0100 1011 0110
0110 0000 0000

Чтобы два двоичных числа образовали квадроключ, их необходимо объединить в число в системе счисления с основанием 4. Сделать это в программном коде без непосредственного перебора битов не удастся, но для наглядности можно просто удвоить все биты широты и сложить два значения так, как если бы они были записаны по основанию 4:

  0100 1011 0110
+ 0220 0000 0020
  --------------
  0320 1011 0130

Полученный квадроключ подставляется на место заполнителя «{quadkey}» в шаблонах URI, полученных от веб-службы. Построенный таким образом URI идентифицирует квадратное растровое изображение со стороной 256 пикселов.

Ниже приведен фрагмент RotatingMap, который строит квадроключ по усеченным целочисленным значениям широты и долготы. Для ясности логика была разделена: сначала строится длинное целое число, а затем строка:

public sealed partial class MainPage : Page
{
    // ...
    
    StringBuilder strBuilder = new StringBuilder();
    
    // ...
    
    private 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();
    }
    
    // ...
}

Квадроключ идентифицирует плитку, содержащую нужную широту и долготу, но место с точными значениями широты и долготы находится где-то в этом файле. Местонахождение пиксела в плитке может быть определено по 8 цифрам целых широты и долготы, следующих за цифрами, необходимыми для квадроключа.

Работа подходит к концу. Так как вся страница должна быть закрыта квадратными плитками со стороной 256 пикселов, а текущая позиция находится в центре экрана где-то в пределах центральной плитки, обработчик SizeChanged определяет, сколько плиток понадобится, а следовательно, сколько элементов Image необходимо создать. Имя поля sqrtNumTiles означает «квадратный корень из количества плиток». Для экрана 1366 x 768 пикселов его значение будет равно 9. Общее количество плиток (и элементов Image) равно квадрату этой величины, то есть 81:

public sealed partial class MainPage : Page
{
    // ...
    
    int sqrtNumTiles;       // всегда нечетное число
    
    // ...
    
    private void OnMainPageSizeChanged(object sender, SizeChangedEventArgs e)
    {
        // Уничтожение существующих элементов Image
        imageCanvas.Children.Clear();

        // Определение количества необходимых элементов Image
        double diagonal = Math.Sqrt(Math.Pow(e.NewSize.Width, 2) +
                            Math.Pow(e.NewSize.Height, 2));

        sqrtNumTiles = 1 + 2 * (int)Math.Ceiling((diagonal / 2) / 256);

        // Создание элементов Image для массива sqrtNumTiles x sqrtNumTiles
        for (int i = 0; i < sqrtNumTiles * sqrtNumTiles; i++)
        {
            Image image = new Image
            {
                Source = new BitmapImage(),
                Stretch = Stretch.None
            };
            imageCanvas.Children.Add(image);
        }
        RefreshDisplay();
    }
    
    // ...
}

Настоящая работа выполняется в методе RefreshDisplay. Он перебирает элементы Image и определяет квадроключ (а следовательно, и URI) для каждого элемента:

public sealed partial class MainPage : Page
{
    // ...
    
    private void RefreshDisplay()
    {
        if (roadParams == null || aerialParams == null)
            return;

        if (integerLongitude == -1 || integerLatitude == -1)
            return;

        // Получение координат и смещений пикселов на основании текущего уровня увеличения
        int croppedLongitude = integerLongitude >> BITRES - zoomLevel;
        int croppedLatitude = integerLatitude >> BITRES - zoomLevel;
        int xPixelOffset = (integerLongitude >> BITRES - zoomLevel - 8) % 256;
        int yPixelOffset = (integerLatitude >> BITRES - zoomLevel - 8) % 256;

        // Подготовка к циклу
        string uriTemplate = (mapStyle == MapStyle.Road ? roadParams : aerialParams).UriTemplate;
        int index = 0;
        int maxValue = (1 << zoomLevel) - 1;

        // Перебор массива элементов Image
        for (int row = -sqrtNumTiles / 2; row <= sqrtNumTiles / 2; row++)
            for (int col = -sqrtNumTiles / 2; col <= sqrtNumTiles / 2; col++)
            {
                // Получение объектов Image и BitmapImage
                Image image = imageCanvas.Children[index] as Image;
                BitmapImage bitmap = image.Source as BitmapImage;
                index++;

                // Проверка выхода за границу
                if (croppedLongitude + col < 0 ||
                    croppedLongitude + col > maxValue ||
                    croppedLatitude + row < 0 ||
                    croppedLatitude + row > maxValue)
                {
                    bitmap.UriSource = null;
                }
                else
                {
                    // Вычисление квадроключа и задание URI
                    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);
                }

                // Позиционирование элемента Image
                Canvas.SetLeft(image, col * 256 - xPixelOffset);
                Canvas.SetTop(image, row * 256 - yPixelOffset);
            }
    }
    
    // ...
}

Остается рассмотреть код, относящийся к кнопкам в строке приложения. Кнопки увеличения и уменьшения блокируются и снова становятся доступными в зависимости от минимального и максимального уровня увеличения для текущего режима, хотя (как я уже упоминал) другие части программы «знают», что максимальный уровень увеличения равен 21:

public sealed partial class MainPage : Page
{
    // ...
    
    private void OnStreetViewAppBarButtonChecked(object sender, RoutedEventArgs e)
    {
        AppBarToggleButton btn = sender as AppBarToggleButton;
        ViewParams viewParams = null;

        if (btn.IsChecked.Value)
        {
            mapStyle = MapStyle.Road;
            viewParams = roadParams;
        }
        else
        {
            mapStyle = MapStyle.Aerial;
            viewParams = aerialParams;
        }

        zoomLevel = Math.Max(viewParams.MinimumLevel,
                Math.Min(viewParams.MaximumLevel, zoomLevel));

        RefreshDisplay();
        RefreshButtons();
    }

    private void OnZoomInAppBarButtonClick(object sender, RoutedEventArgs e)
    {
        zoomLevel += 1;
        RefreshDisplay();
        RefreshButtons();
    }

    private void OnZoomOutAppBarButtonClick(object sender, RoutedEventArgs e)
    {
        zoomLevel -= 1;
        RefreshDisplay();
        RefreshButtons();
    }

    private void RefreshButtons()
    {
        ViewParams viewParams = streetViewAppBarButton.IsChecked.Value ? roadParams : aerialParams;
        zoomInAppBarButton.IsEnabled = zoomLevel < viewParams.MaximumLevel;
        zoomOutAppBarButton.IsEnabled = zoomLevel > viewParams.MinimumLevel;
    }
}

Знакомые области карты после поворота выглядят непривычно — как, например, остров Манхэттен на следующем снимке:

Использование карты Bing в приложении Windows Runtime

Но если вы стоите в незнакомом месте с планшетом и пытаетесь разобраться, где оказались, вращение карты вместе с вашим перемещением может быть очень полезным. Возможно, когда-нибудь метки с названиями городов и улиц тоже будут автоматически поворачиваться.

Пройди тесты
Лучший чат для C# программистов