Уровни увеличения и сетка фрагментов

Azure Карты использовать систему координат проекции Spherical Mercator (EPSG:3857). Проекция — это математическая модель, используемая для преобразования сферического земного шара в плоскую карту. Сферическая проекция Меркатора растягивает карту возле полюсов, чтобы получить прямоугольную карту. Эта проекция существенно искажает масштаб и соотношение площадей на карте, но компенсирует это двумя важными аспектами:

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

Чтобы оптимизировать производительность операций извлечения и отображения карт, каждая карта делится на квадратные фрагменты. В SDK Azure Maps используются фрагменты размером 512×512 пикселей для дорожных карт и более мелкие фрагменты (256×256 пикселей) для спутниковых снимков. Azure Maps предоставляет растровые и векторные фрагменты для 23 уровней масштаба (от 0 до 22). На нулевом уровне масштаба вся карта мира помещается на одной плитке:

Фрагмент карты мира

На первом уровне масштаба карта мира отображается в 2 фрагментах: квадрат 2 x 2.

Макет с фрагментами карты 2×2

На каждом дополнительном уровне масштаба фрагменты предыдущего уровня разделяются на четыре фрагмента, создавая сетку 2масштаб × 2масштаб. 22-й уровень масштаба — это сетка 222 x 222, или 4 194 304 x 4 194 304 фрагмента (всего 17 592 186 044 416 фрагментов).

Интерактивные элементы управления картой в Azure Maps для браузера и Android поддерживают 25 уровней масштаба (от 0 до 24). Хотя дорожные данные доступны только на уровнях масштабирования, когда плитки доступны.

В следующей таблице представлен полный список значений для уровней масштабирования, где размер плитки составляет 256 пикселей:

Уровень масштаба м/пиксель м/сторона фрагмента
0 156 543 40 075 017
1 78 271,5 20 037 508
2 39 135,8 10 018 754
3 19 567,88 5 009 377,1
4 9783,94 2 504 688,5
5 4891,97 1 252 344,3
6 2445,98 626 172,1
7 1222,99 313 086,1
8 611,5 156 543
9 305,75 78 271,5
10 152,87 39 135,8
11 76,44 19 567,9
12 38,219 9783,94
13 19,109 4891,97
14 9,555 2445,98
15 4,777 1222,99
16 2,3887 611,496
17 1,1943 305,748
18 0,5972 152,874
19 0,2986 76,437
20 0,14929 38,2185
21 0,074646 19,10926
22 0,037323 9,55463
23 0,0186615 4,777315
24 0,00933075 2,3886575

Координаты в пикселях

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

var mapWidth = tileSize * Math.pow(2, zoom);

var mapHeight = mapWidth;

Так как ширина и высота карты будут разными на каждом уровне масштаба, изменятся и координаты в пикселях. Пиксель в левом верхнем углу карты всегда имеет координаты в пикселях (0, 0). Пиксель в правом нижнем углу на карте имеет координаты в пикселях (ширина – 1, высота – 1), что можно выразить в переменных и уравнениях из предыдущего раздела как (сторона фрагмента 2масштаб – 1, сторона фрагмента – 2масштаб – 1). Например, при использовании 512 квадратных фрагментов на уровне 2 координаты в пикселях будут находиться в диапазоне от (0, 0) до (2047, 2047), как показано ниже:

Карта с обозначением размеров в пикселях

Зная широту и долготу в градусах, а также уровень масштаба, мы можем вычислить координаты в пикселях по осям X и Y следующим образом.

var sinLatitude = Math.sin(latitude * Math.PI/180);

var pixelX = ((longitude + 180) / 360) * tileSize * Math.pow(2, zoom);

var pixelY = (0.5 – Math.log((1 + sinLatitude) / (1 – sinLatitude)) / (4 * Math.PI)) * tileSize * Math.pow(2, zoom);

Предполагается, что значения широты и долготы приводятся в стандарте WGS 84. Хотя Azure Maps использует сферическую проекцию, очень важно правильно перевести все географические координаты в общий стандарт. Мы выбрали стандарт WGS 84. Значение долготы находится в диапазоне от -180 до +180 градусов, а значение широты обрезается до диапазона от -85,05112878 до +85,05112878 градусов. Соблюдение этого стандарта позволяет избежать сингулярностей на полюсах и обеспечивает прямоугольную форму проецируемой карты.

Координаты фрагментов

Чтобы оптимизировать производительность операций извлечения и отображения карт, отображаемая карта разрезается на фрагменты. Количество пикселей и число фрагментов будут разными на каждом уровне масштаба.

var numberOfTilesWide = Math.pow(2, zoom);

var numberOfTilesHigh = numberOfTilesWide;

Каждому фрагменту присваиваются координаты в диапазоне от (0,0) в верхнем левом углу до (2масштаб - 1, 2масштаб - 1) в правом нижнем углу. Например, на уровне масштабирования 3 координаты фрагмента находятся в диапазоне от (0, 0) до (7, 7), как показано ниже:

Схема координат фрагмента

Получив пару координат (X, Y) в пикселях, можно легко определить координаты (X, Y) того фрагмента, который содержит этот пиксель:

var tileX = Math.floor(pixelX / tileSize);

var tileY = Math.floor(pixelY / tileSize);

Фрагменты вызываются по уровню масштаба. Координаты (X, Y) соответствуют положению фрагмента на сетке карты для этого уровня масштаба.

При выборе уровня масштаба помните, что каждое положение — это фиксированная позиция на соответствующем фрагменте. Это означает, что количество фрагментов, требуемое для отображения заданной территории, зависит от конкретного размещения сетки масштабирования на карте мира. Например, есть две точки, расстояние между которыми составляет 900 метров. Для отображения этого маршрута может понадобиться только три плитки на 17-ом уровне масштабирования. Но если западная точка находится а правой части своего фрагмента, а восточная — в левой части своего, может понадобиться четыре фрагмента:

Демонстрация увеличения масштаба

После выбора уровня масштаба вычисляются значения x и y. Верхний фрагмент слева на каждой сетке масштаба имеет такие координаты: x = 0, y = 0. Нижний фрагмент справа имеет такие координаты: x = 2масштаб - 1, y = 2масштаб - 1.

Ниже приведена сетка масштабирования для уровня 1.

Сетка масштаба для первого уровня масштаба

Индексы Quadkey

Некоторые платформы карт используют соглашение об именовании индексирования quadkey, в котором координаты (X, Y) фрагмента объединяются в одномерную строку, называемую ключами quadtree или quadkeys. Каждый экземпляр quadkey однозначно определяет один фрагмент на определенном уровне детализации и его можно использовать в качестве ключа во многих распространенных индексах сбалансированного дерева базы данных. Пакеты SDK Azure Maps поддерживают наложение слоев фрагментов, использующих соглашение об именовании quadkey, в дополнение к другим соглашениям об именовании, как описано в статье Добавление слоя фрагментов.

Примечание.

Соглашение об именовании quadkeys работает только для уровней масштаба 1 и выше. Пакеты SDK Azure Maps поддерживают уровень масштаба 0, когда весь мир отображается на одном фрагменте карты.

Чтобы преобразовать координаты фрагмента в формат quadkey, биты координат Y и X чередуются, а результат интерпретируется как число с основанием 4 с сохранением ведущих нулей и преобразуется в строку. Например, если координаты (X, Y) фрагмента имеют значение (3, 5) на уровне 3, quadkey вычисляется следующим образом:

tileX = 3 = 011 (base 2)

tileY = 5 = 101 (base 2)

quadkey = 100111 (base 2) = 213 (base 4) = "213"

У Qquadkeys есть несколько интересных свойств. Во-первых, длина quadkey (число цифр) равна уровню масштаба для соответствующего фрагмента. Во-вторых, элемент quadkey для любого фрагмента начинается с quadkey его родительского фрагмента (который содержит текущий фрагмент на предыдущем уровне). Как показано в следующем примере, плитка 2 является родительским элементом плиток 20–23:

Пирамида фрагментов Quadkey

И наконец, quadkeys предоставляет одномерный ключ индекса, который обычно сохраняет информацию о близости фрагментов в пространстве координат XY. Другими словами, два фрагмента с близко расположенными координатами XY обычно имеют достаточно близкие значения quadkeys. Это важно для оптимизации производительности базы данных, так как соседние фрагменты часто запрашиваются группами и их лучше хранить в одном блоке диска, чтобы свести к минимуму число операций чтения с диска.

Исходный код математических расчетов для фрагментов

В следующем примере кода показано, как реализовать описанные в этом документе функции. Эти функции можно легко преобразовать для других языков программирования по мере необходимости.

using System;
using System.Text;

namespace AzureMaps
{
    /// <summary>
    /// Tile System math for the Spherical Mercator projection coordinate system (EPSG:3857)
    /// </summary>
    public static class TileMath
    {
        //Earth radius in meters.
        private const double EarthRadius = 6378137;

        private const double MinLatitude = -85.05112878;
        private const double MaxLatitude = 85.05112878;
        private const double MinLongitude = -180;
        private const double MaxLongitude = 180;

        /// <summary>
        /// Clips a number to the specified minimum and maximum values.
        /// </summary>
        /// <param name="n">The number to clip.</param>
        /// <param name="minValue">Minimum allowable value.</param>
        /// <param name="maxValue">Maximum allowable value.</param>
        /// <returns>The clipped value.</returns>
        private static double Clip(double n, double minValue, double maxValue)
        {
            return Math.Min(Math.Max(n, minValue), maxValue);
        }

        /// <summary>
        /// Calculates width and height of the map in pixels at a specific zoom level from -180 degrees to 180 degrees.
        /// </summary>
        /// <param name="zoom">Zoom Level to calculate width at</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <returns>Width and height of the map in pixels</returns>
        public static double MapSize(double zoom, int tileSize)
        {
            return Math.Ceiling(tileSize * Math.Pow(2, zoom));
        }

        /// <summary>
        /// Calculates the Ground resolution at a specific degree of latitude in meters per pixel.
        /// </summary>
        /// <param name="latitude">Degree of latitude to calculate resolution at</param>
        /// <param name="zoom">Zoom level to calculate resolution at</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <returns>Ground resolution in meters per pixels</returns>
        public static double GroundResolution(double latitude, double zoom, int tileSize)
        {
            latitude = Clip(latitude, MinLatitude, MaxLatitude);
            return Math.Cos(latitude * Math.PI / 180) * 2 * Math.PI * EarthRadius / MapSize(zoom, tileSize);
        }

        /// <summary>
        /// Determines the map scale at a specified latitude, level of detail, and screen resolution.
        /// </summary>
        /// <param name="latitude">Latitude (in degrees) at which to measure the map scale.</param>
        /// <param name="zoom">Level of detail, from 1 (lowest detail) to 23 (highest detail).</param>
        /// <param name="screenDpi">Resolution of the screen, in dots per inch.</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <returns>The map scale, expressed as the denominator N of the ratio 1 : N.</returns>
        public static double MapScale(double latitude, double zoom, int screenDpi, int tileSize)
        {
            return GroundResolution(latitude, zoom, tileSize) * screenDpi / 0.0254;
        }

        /// <summary>
        /// Global Converts a Pixel coordinate into a geospatial coordinate at a specified zoom level. 
        /// Global Pixel coordinates are relative to the top left corner of the map (90, -180)
        /// </summary>
        /// <param name="pixel">Pixel coordinates in the format of [x, y].</param>  
        /// <param name="zoom">Zoom level</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <returns>A position value in the format [longitude, latitude].</returns>
        public static double[] GlobalPixelToPosition(double[] pixel, double zoom, int tileSize)
        {
            var mapSize = MapSize(zoom, tileSize);

            var x = (Clip(pixel[0], 0, mapSize - 1) / mapSize) - 0.5;
            var y = 0.5 - (Clip(pixel[1], 0, mapSize - 1) / mapSize);

            return new double[] {
                360 * x,    //Longitude
                90 - 360 * Math.Atan(Math.Exp(-y * 2 * Math.PI)) / Math.PI  //Latitude
            };
        }

        /// <summary>
        /// Converts a point from latitude/longitude WGS-84 coordinates (in degrees) into pixel XY coordinates at a specified level of detail.
        /// </summary>
        /// <param name="position">Position coordinate in the format [longitude, latitude]</param>
        /// <param name="zoom">Zoom level.</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param> 
        /// <returns>A global pixel coordinate.</returns>
        public static double[] PositionToGlobalPixel(double[] position, int zoom, int tileSize)
        {
            var latitude = Clip(position[1], MinLatitude, MaxLatitude);
            var longitude = Clip(position[0], MinLongitude, MaxLongitude);

            var x = (longitude + 180) / 360;
            var sinLatitude = Math.Sin(latitude * Math.PI / 180);
            var y = 0.5 - Math.Log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI);

            var mapSize = MapSize(zoom, tileSize);

            return new double[] {
                 Clip(x * mapSize + 0.5, 0, mapSize - 1),
                 Clip(y * mapSize + 0.5, 0, mapSize - 1)
            };
        }

        /// <summary>
        /// Converts pixel XY coordinates into tile XY coordinates of the tile containing the specified pixel.
        /// </summary>
        /// <param name="pixel">Pixel coordinates in the format of [x, y].</param>  
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <param name="tileX">Output parameter receiving the tile X coordinate.</param>
        /// <param name="tileY">Output parameter receiving the tile Y coordinate.</param>
        public static void GlobalPixelToTileXY(double[] pixel, int tileSize, out int tileX, out int tileY)
        {
            tileX = (int)(pixel[0] / tileSize);
            tileY = (int)(pixel[1] / tileSize);
        }

        /// <summary>
        /// Performs a scale transform on a global pixel value from one zoom level to another.
        /// </summary>
        /// <param name="pixel">Pixel coordinates in the format of [x, y].</param>  
        /// <param name="oldZoom">The zoom level in which the input global pixel value is from.</param>  
        /// <returns>A scale pixel coordinate.</returns>
        public static double[] ScaleGlobalPixel(double[] pixel, double oldZoom, double newZoom)
        {
            var scale = Math.Pow(2, oldZoom - newZoom);

            return new double[] { pixel[0] * scale, pixel[1] * scale };
        }

        /// <summary>
        /// Performs a scale transform on a set of global pixel values from one zoom level to another.
        /// </summary>
        /// <param name="pixels">A set of global pixel value from the old zoom level. Points are in the format [x,y].</param>
        /// <param name="oldZoom">The zoom level in which the input global pixel values is from.</param>
        /// <param name="newZoom">The new zoom level in which the output global pixel values should be aligned with.</param>
        /// <returns>A set of global pixel values that has been scaled for the new zoom level.</returns>
        public static double[][] ScaleGlobalPixels(double[][] pixels, double oldZoom, double newZoom)
        {
            var scale = Math.Pow(2, oldZoom - newZoom);

            var output = new System.Collections.Generic.List<double[]>();
            foreach (var p in pixels)
            {
                output.Add(new double[] { p[0] * scale, p[1] * scale });
            }

            return output.ToArray();
        }

        /// <summary>
        /// Converts tile XY coordinates into a global pixel XY coordinates of the upper-left pixel of the specified tile.
        /// </summary>
        /// <param name="tileX">Tile X coordinate.</param>
        /// <param name="tileY">Tile Y coordinate.</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <param name="pixelX">Output parameter receiving the X coordinate of the point, in pixels.</param>  
        /// <param name="pixelY">Output parameter receiving the Y coordinate of the point, in pixels.</param>  
        public static double[] TileXYToGlobalPixel(int tileX, int tileY, int tileSize)
        {
            return new double[] { tileX * tileSize, tileY * tileSize };
        }

        /// <summary>
        /// Converts tile XY coordinates into a quadkey at a specified level of detail.
        /// </summary>
        /// <param name="tileX">Tile X coordinate.</param>
        /// <param name="tileY">Tile Y coordinate.</param>
        /// <param name="zoom">Zoom level</param>
        /// <returns>A string containing the quadkey.</returns>
        public static string TileXYToQuadKey(int tileX, int tileY, int zoom)
        {
            var quadKey = new StringBuilder();
            for (int i = zoom; i > 0; i--)
            {
                char digit = '0';
                int mask = 1 << (i - 1);
                if ((tileX & mask) != 0)
                {
                    digit++;
                }
                if ((tileY & mask) != 0)
                {
                    digit++;
                    digit++;
                }
                quadKey.Append(digit);
            }
            return quadKey.ToString();
        }

        /// <summary>
        /// Converts a quadkey into tile XY coordinates.
        /// </summary>
        /// <param name="quadKey">Quadkey of the tile.</param>
        /// <param name="tileX">Output parameter receiving the tile X coordinate.</param>
        /// <param name="tileY">Output parameter receiving the tile Y coordinate.</param>
        /// <param name="zoom">Output parameter receiving the zoom level.</param>
        public static void QuadKeyToTileXY(string quadKey, out int tileX, out int tileY, out int zoom)
        {
            tileX = tileY = 0;
            zoom = quadKey.Length;
            for (int i = zoom; i > 0; i--)
            {
                int mask = 1 << (i - 1);
                switch (quadKey[zoom - i])
                {
                    case '0':
                        break;

                    case '1':
                        tileX |= mask;
                        break;

                    case '2':
                        tileY |= mask;
                        break;

                    case '3':
                        tileX |= mask;
                        tileY |= mask;
                        break;

                    default:
                        throw new ArgumentException("Invalid QuadKey digit sequence.");
                }
            }
        }

        /// <summary>
        /// Calculates the XY tile coordinates that a coordinate falls into for a specific zoom level.
        /// </summary>
        /// <param name="position">Position coordinate in the format [longitude, latitude]</param>
        /// <param name="zoom">Zoom level</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <param name="tileX">Output parameter receiving the tile X position.</param>
        /// <param name="tileY">Output parameter receiving the tile Y position.</param>
        public static void PositionToTileXY(double[] position, int zoom, int tileSize, out int tileX, out int tileY)
        {
            var latitude = Clip(position[1], MinLatitude, MaxLatitude);
            var longitude = Clip(position[0], MinLongitude, MaxLongitude);

            var x = (longitude + 180) / 360;
            var sinLatitude = Math.Sin(latitude * Math.PI / 180);
            var y = 0.5 - Math.Log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI);

            //tileSize needed in calculations as in rare cases the multiplying/rounding/dividing can make the difference of a pixel which can result in a completely different tile. 
            var mapSize = MapSize(zoom, tileSize);
            tileX = (int)Math.Floor(Clip(x * mapSize + 0.5, 0, mapSize - 1) / tileSize);
            tileY = (int)Math.Floor(Clip(y * mapSize + 0.5, 0, mapSize - 1) / tileSize);
        }

        /// <summary>
        /// Calculates the tile quadkey strings that are within a specified viewport.
        /// </summary>
        /// <param name="position">Position coordinate in the format [longitude, latitude]</param>
        /// <param name="zoom">Zoom level</param>
        /// <param name="width">The width of the map viewport in pixels.</param>
        /// <param name="height">The height of the map viewport in pixels.</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <returns>A list of quadkey strings that are within the specified viewport.</returns>
        public static string[] GetQuadkeysInView(double[] position, int zoom, int width, int height, int tileSize)
        {
            var p = PositionToGlobalPixel(position, zoom, tileSize);

            var top = p[1] - height * 0.5;
            var left = p[0] - width * 0.5;

            var bottom = p[1] + height * 0.5;
            var right = p[0] + width * 0.5;

            var tl = GlobalPixelToPosition(new double[] { left, top }, zoom, tileSize);
            var br = GlobalPixelToPosition(new double[] { right, bottom }, zoom, tileSize);

            //Boudning box in the format: [west, south, east, north];
            var bounds = new double[] { tl[0], br[1], br[0], tl[1] };

            return GetQuadkeysInBoundingBox(bounds, zoom, tileSize);
        }

        /// <summary>
        /// Calculates the tile quadkey strings that are within a bounding box at a specific zoom level.
        /// </summary>
        /// <param name="bounds">A bounding box defined as an array of numbers in the format of [west, south, east, north].</param>
        /// <param name="zoom">Zoom level to calculate tiles for.</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <returns>A list of quadkey strings.</returns>
        public static string[] GetQuadkeysInBoundingBox(double[] bounds, int zoom, int tileSize)
        {
            var keys = new System.Collections.Generic.List<string>();

            if (bounds != null && bounds.Length >= 4)
            {
                PositionToTileXY(new double[] { bounds[3], bounds[0] }, zoom, tileSize, out int tlX, out int tlY);
                PositionToTileXY(new double[] { bounds[1], bounds[2] }, zoom, tileSize, out int brX, out int brY);

                for (int x = tlX; x <= brX; x++)
                {
                    for (int y = tlY; y <= brY; y++)
                    {
                        keys.Add(TileXYToQuadKey(x, y, zoom));
                    }
                }
            }

            return keys.ToArray();
        }

        /// <summary>
        /// Calculates the bounding box of a tile.
        /// </summary>
        /// <param name="tileX">Tile X coordinate</param>
        /// <param name="tileY">Tile Y coordinate</param>
        /// <param name="zoom">Zoom level</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <returns>A bounding box of the tile defined as an array of numbers in the format of [west, south, east, north].</returns>
        public static double[] TileXYToBoundingBox(int tileX, int tileY, double zoom, int tileSize)
        {
            //Top left corner pixel coordinates
            var x1 = (double)(tileX * tileSize);
            var y1 = (double)(tileY * tileSize);

            //Bottom right corner pixel coordinates
            var x2 = (double)(x1 + tileSize);
            var y2 = (double)(y1 + tileSize);

            var nw = GlobalPixelToPosition(new double[] { x1, y1 }, zoom, tileSize);
            var se = GlobalPixelToPosition(new double[] { x2, y2 }, zoom, tileSize);

            return new double[] { nw[0], se[1], se[0], nw[1] };
        }

        /// <summary>
        /// Calculates the best map view (center, zoom) for a bounding box on a map.
        /// </summary>
        /// <param name="bounds">A bounding box defined as an array of numbers in the format of [west, south, east, north].</param>
        /// <param name="mapWidth">Map width in pixels.</param>
        /// <param name="mapHeight">Map height in pixels.</param>
        /// <param name="padding">Width in pixels to use to create a buffer around the map. This is to keep markers from being cut off on the edge</param>
        /// <param name="tileSize">The size of the tiles in the tile pyramid.</param>
        /// <param name="latitude">Output parameter receiving the center latitude coordinate.</param>
        /// <param name="longitude">Output parameter receiving the center longitude coordinate.</param>
        /// <param name="zoom">Output parameter receiving the zoom level</param>
        public static void BestMapView(double[] bounds, double mapWidth, double mapHeight, int padding, int tileSize, out double centerLat, out double centerLon, out double zoom)
        {
            if (bounds == null || bounds.Length < 4)
            {
                centerLat = 0;
                centerLon = 0;
                zoom = 1;
                return;
            }

            double boundsDeltaX;

            //Check if east value is greater than west value which would indicate that bounding box crosses the antimeridian.
            if (bounds[2] > bounds[0])
            {
                boundsDeltaX = bounds[2] - bounds[0];
                centerLon = (bounds[2] + bounds[0]) / 2;
            }
            else
            {
                boundsDeltaX = 360 - (bounds[0] - bounds[2]);
                centerLon = ((bounds[2] + bounds[0]) / 2 + 360) % 360 - 180;
            }

            var ry1 = Math.Log((Math.Sin(bounds[1] * Math.PI / 180) + 1) / Math.Cos(bounds[1] * Math.PI / 180));
            var ry2 = Math.Log((Math.Sin(bounds[3] * Math.PI / 180) + 1) / Math.Cos(bounds[3] * Math.PI / 180));
            var ryc = (ry1 + ry2) / 2;

            centerLat = Math.Atan(Math.Sinh(ryc)) * 180 / Math.PI;

            var resolutionHorizontal = boundsDeltaX / (mapWidth - padding * 2);

            var vy0 = Math.Log(Math.Tan(Math.PI * (0.25 + centerLat / 360)));
            var vy1 = Math.Log(Math.Tan(Math.PI * (0.25 + bounds[3] / 360)));
            var zoomFactorPowered = (mapHeight * 0.5 - padding) / (40.7436654315252 * (vy1 - vy0));
            var resolutionVertical = 360.0 / (zoomFactorPowered * tileSize);

            var resolution = Math.Max(resolutionHorizontal, resolutionVertical);

            zoom = Math.Log(360 / (resolution * tileSize), 2);
        }
    }
}

Примечание.

Интерактивные элементы управления картой в пакете SDK Azure Maps дополнены вспомогательными функциями для преобразования геопространственных положений и пикселей в окне просмотра.

Следующие шаги

Вы можете напрямую обращаться к фрагментам карты через службы REST Azure Maps:

См. дополнительные сведения о концепциях геопространственных данных: