縮放層級和圖格格線
Azure 地圖服務使用麥卡托圓球投影座標系統 (EPSG:3857)。 投影是用來將球面地球轉換成平面地圖的數學模型。 球面麥卡托 (Spherical Mercator) 投影法會延展兩極的地圖,以建立方形地圖。 此投影會大幅扭曲地圖的縮放比例和區域,但相較於此失真,有兩個重要屬性:
- 這是共形投影,這表示其會保留相對小型物件的形狀。 顯示空拍影像時,保留小型物件的形狀特別重要。 例如,我們想要避免扭曲建築物的形狀。 正方形建築物應該會顯示為正方形,而非矩形。
- 這是一個圓柱形投影。 北方和南方一律是上下,而西方和東方一律是左右。
若要將地圖擷取和顯示的效能最佳化,地圖會分割成正方形圖格。 Azure 地圖服務 SDK 對道路地圖使用大小為 512 x 512 像素的圖格,且對衛星影像使用較小的 256 x 256 像素。 Azure 地圖服務提供 23 個縮放層級的點陣和向量圖格,編號 0 到 22。 在縮放層級 0,整個世界剛好放進單一圖格裡:
縮放層級 1 使用四個圖格來呈現世界:2 x 2 個正方形
每個額外的縮放層級再將前一層級的圖格一分為四,建立 2縮放 x 2縮放 的格線。 縮放層級 22 是 格線 222 x 222,或 4,194,304 x 4,194,304 個圖格 (總計 17,592,186,044,416 個圖格)。
Web 和 Android 的 Azure 地圖服務互動式地圖控制項支援 25 個縮放層級,編號 0 到 24。 不過,道路資料只在圖格可用時才可在縮放層級取得。
下表提供縮放層級的完整值清單,其中磚大小為 256 像素平方:
縮放層級 | 公尺/像素 | 公尺/圖格邊 |
---|---|---|
0 | 156543 | 40075017 |
1 | 78271.5 | 20037508 |
2 | 39135.8 | 10018754 |
3 | 19567.88 | 5009377.1 |
4 | 9783.94 | 2504688.5 |
5 | 4891.97 | 1252344.3 |
6 | 2445.98 | 626172.1 |
7 | 1222.99 | 313086.1 |
8 | 611.5 | 156543 |
9 | 305.75 | 78271.5 |
10 | 152.87 | 39135.8 |
11 | 76.44 | 19567.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)。 地圖右下角的像素會有像素座標 (width-1, height-1),或參照上一節中的方程式,(tileSize * 2zoom–1, tileSize * 2zoom–1)。 例如,在層級 2 使用 512 正方形圖格時,像素座標範圍會從 (0, 0) 到 (2047, 2047),如下所示:
根據緯度和經度,以及詳細資料層級,像素 XY 座標的計算方式如下:
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 地圖服務使用球面投影,請務必將所有地理座標轉換成通用資材。 WGS 84 是選取的資材。 經度值假設範圍為 -180 度到 +180 度,且緯度值必須裁剪為範圍從 -85.05112878 到 85.05112878。 遵守這些值可避免極點的奇異點,並可確保投影的地圖是正方形形狀。
圖格座標
為了將地圖擷取和顯示效能最佳化,轉譯的地圖會切割成圖格。 每個縮放層級的像素數目和圖格數目不同:
var numberOfTilesWide = Math.pow(2, zoom);
var numberOfTilesHigh = numberOfTilesWide;
每個圖格都有 XY 座標,範圍從左上角的 (0, 0) 到右下角的 (2zoom–1, 2zoom–1)。 例如,在縮放層級 3 中,圖格座標的範圍從 (0, 0) 到 (7, 7),如下所示:
假設有一組像素 XY 座標,您可以輕鬆地判斷包含該像素之圖格的圖格 XY 座標:
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
索引命名慣例,其將圖格 ZY 座標結合成稱為 quadtree
索引鍵或 quadkeys
(簡稱) 的一維字串。 每個 quadkey
可在特定詳細層級唯一識別單一圖格,而且可作為常見的資料庫 B 型樹狀結構索引中的索引鍵。 Azure 地圖服務 SDK 除了新增圖格圖層文件中所記載的其他命名慣例之外,還支援使用 quadkey
命名慣例的圖格圖層重疊。
注意
quadkeys
命名慣例僅適用一個或更大的縮放層級。 Azure 地圖服務 SDK 支援縮放層級 0,這是全球的單一地圖圖格。
若要將圖格座標轉換成 quadkey
,Y 和 X 座標的位元會交錯,而且結果會解譯為基底 4 數字 (保有前置零) 並轉換成字串。 例如,在層級 3 指定圖格 XY 座標 (3, 5),決定的 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 的父代:
最後,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);
}
}
}
下一步
直接從 Azure 地圖服務 REST 服務存取地圖圖格:
深入了解地理空間概念: