如何计算不同地图缩放级别下边界框的像素宽度和高度

75 阅读6分钟

在创建打印地图或静态地图图像时,关键挑战之一是控制比例:我感兴趣的区域的宽度和高度是多少像素?

假设你有一个由两个经纬度角定义的边界框。根据缩放比例,它的宽度和高度(以像素为单位)会发生变化。有时你知道缩放比例,并想测量像素大小;有时你知道所需的图像宽度(或高度),并想找到适合该边界框的缩放比例。

这正是您在使用Geoapify 静态地图 API时所需要的— 例如:

以特定尺寸打印地图, 生成静态图像,其中标签保持可读性, 确保请求的区域完全适合图像尺寸。 在本文中,我们将通过简单的 JavaScript 帮助程序和现场演示介绍三种场景:

计算给定缩放比例下的 bbox 宽度和高度。 当高度固定时计算宽度。 宽度固定时计算高度。 在演示中尝试一下:

背景:缩放级别、纬度/经度和像素 大多数现代地图库(例如Leaflet、MapLibre GL、Google Maps和OpenLayers)都依赖于Web 墨卡托投影(EPSG:3857)。该​​投影将地理坐标(经纬度)转换为平面方形地图,可平铺显示在屏幕上。

它的工作原理如下:

经度与 X 轴呈线性映射关系。缩放级别 0 时,整个范围 −180° … +180° 可容纳在一个 256 像素的图块中。 纬度采用墨卡托公式进行投影,该公式拉伸两极附近的距离。为了避免出现无穷大的值,地图被裁剪至纬度±85.05113°左右。 墨卡托投影与各国实际相对大小之间的关系。作者:Jakub Nowosad。来源: CC BY-SA 4.0许可:

GIF 动画显示墨卡托投影对国家大小的扭曲

结果是一个由图块组成的网格,每个图块通常为 256×256 像素:

在缩放比例为 0 时,世界是一个图块(宽 256 像素)。 在缩放 1 时,世界宽度为 512 像素(2 个图块 × 256)。 缩放至2 时,宽度为 1024 像素。 一般来说: worldSizePx = 256 × 2^z 因此,每次缩放都会使经度和纬度的可用像素数加倍。

👉 实时查看:缩放拼贴演示(切换网格以查看图块边界)。👉 阅读更多:了解地图缩放级别和 XYZ 图块坐标。

这种从纬度/经度到像素的转换解释了为什么边界框的像素宽度/高度取决于缩放比例。赤道附近 1° × 1° 的区域在缩放比例为 2 时可能只跨越几个像素,但在缩放比例为 12 时则可能跨越数千个像素——而在高纬度地区,墨卡托投影会进一步夸大这种效果。

这个基础为我们应对三种实际场景做好了准备:

计算给定缩放比例的边界框的像素宽度/高度。 给定固定的图像高度,找到相应的宽度。 给定固定的图像宽度,找到相应的高度。 情况 1:给定缩放比例下的宽度和高度 最简单的情况:您已经知道缩放级别,并想测量地理边界框的宽度和高度是多少像素。

这需要将经纬度转换为Web 墨卡托投影中的世界像素坐标。其数学计算如下:

经度 → X(线性): x = (lon + 180) / 360 × worldSize 纬度 → Y(墨卡托投影): y = (0.5 - log((1 + sinφ) / (1 - sinφ)) / (4π)) × worldSize 其中 φ 是以弧度表示的纬度(即φ = lat × π / 180)。

这是 JavaScript 中的一个小实用函数:

const TILE_SIZE = 256;

// Convert lon/lat to world pixel coordinates at zoom function lonLatToWorldPixels(lon, lat, zoom, tileSize = TILE_SIZE) { const scale = tileSize * Math.pow(2, zoom); const x = (lon + 180) / 360 * scale;

const sin = Math.sin((lat * Math.PI) / 180); const y = (0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * scale;

return { x, y }; }

// Calculate bbox width/height in pixels at a given zoom function bboxSizePx(bbox, zoom, tileSize = TILE_SIZE) { const [minLon, minLat, maxLon, maxLat] = bbox; const p1 = lonLatToWorldPixels(minLon, minLat, zoom, tileSize); const p2 = lonLatToWorldPixels(maxLon, maxLat, zoom, tileSize); return { width: Math.abs(p2.x - p1.x), height: Math.abs(p2.y - p1.y) }; } 例子 让我们测量一下巴黎周围的一个方框:

const bboxParis = [2.1, 48.7, 2.5, 49.0]; // [minLon, minLat, maxLon, maxLat]

console.log(bboxSizePx(bboxParis, 5)); // → { width: 9.10 px, height: 10.37 px }

console.log(bboxSizePx(bboxParis, 10)); // → { width: 291.27 px, height: 331.98 px } 在缩放比例为 5 时,巴黎的边界框只有几个像素宽。在缩放比例为 10 时,它已经有几百个像素宽了。

这解释了为什么在打印或生成静态图像时,选择正确的缩放比例至关重要:太低,区域看起来很小,太高,它就不适合图像尺寸。

情况 2:给定高度的宽度 有时你不会直接固定缩放比例——而是需要知道图片的高度(以像素为单位)。根据这个高度,你可以推导出边界框的相应宽度(或者说,决定该高度的缩放比例)。

数学上说得通,因为在 Web Mercator 中,宽度和高度都与 成比例缩放2^z。如果知道一个维度,就可以计算缩放比例,然后推导出另一个维度。

JavaScript 助手 // Precompute bbox deltas at zoom 0 function bboxDeltaAtZoom0(bbox, tileSize = TILE_SIZE) { const [minLon, minLat, maxLon, maxLat] = bbox;

const x0a = tileSize * (minLon + 180) / 360; const x0b = tileSize * (maxLon + 180) / 360; const dx0 = Math.abs(x0b - x0a);

const mercY = (lat) => { const s = Math.sin((lat * Math.PI) / 180); return (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * tileSize; }; const dy0 = Math.abs(mercY(maxLat) - mercY(minLat));

return { dx0, dy0 }; }

// Given a bbox and target height, find zoom function zoomForHeight(bbox, targetHeightPx, tileSize = TILE_SIZE) { const { dy0 } = bboxDeltaAtZoom0(bbox, tileSize); return Math.log2(targetHeightPx / dy0); }

// From that zoom, calculate width function widthFromHeight(bbox, targetHeightPx, tileSize = TILE_SIZE) { const z = zoomForHeight(bbox, targetHeightPx, tileSize); const { width } = bboxSizePx(bbox, z, tileSize); return { zoom: z, width }; } 例子 const bboxParis = [2.1, 48.7, 2.5, 49.0]; // Paris bbox

const result = widthFromHeight(bboxParis, 600); console.log(result); // → { zoom: 10.854, width: 526.42 px } 这告诉我们:

如果我们想要巴黎的 bbox高度为 600 像素,我们需要大约缩放 10.9。 放大后,其宽度约为 526 像素。 为什么它有用 当您知道图像高度(例如,固定的纸张尺寸或纵向的静态地图)并希望宽度相应缩放时,这种情况很方便。

情况 3:给定宽度计算高度 在许多布局中,图像宽度是固定的(例如网站容器、打印列)。您可以先根据该宽度解决缩放问题,然后再计算边界框的高度。

JavaScript 助手 // Precompute bbox deltas at zoom 0 (reuse if already defined) function bboxDeltaAtZoom0(bbox, tileSize = 256) { const [minLon, minLat, maxLon, maxLat] = bbox;

const x0a = tileSize * (minLon + 180) / 360; const x0b = tileSize * (maxLon + 180) / 360; const dx0 = Math.abs(x0b - x0a);

const mercY = (lat) => { const s = Math.sin((lat * Math.PI) / 180); return (0.5 - Math.log((1 + s) / (1 - s)) / (4 * Math.PI)) * tileSize; }; const dy0 = Math.abs(mercY(maxLat) - mercY(minLat));

return { dx0, dy0 }; }

// Solve zoom for a target pixel width function zoomForWidth(bbox, targetWidthPx, tileSize = 256) { const { dx0 } = bboxDeltaAtZoom0(bbox, tileSize); return Math.log2(targetWidthPx / dx0); }

// From that zoom, calculate height function heightFromWidth(bbox, targetWidthPx, tileSize = 256) { const z = zoomForWidth(bbox, targetWidthPx, tileSize); const { height } = bboxSizePx(bbox, z, tileSize); // reuse bboxSizePx from Case 1 return { zoom: z, height }; } 例子 const bboxParis = [2.1, 48.7, 2.5, 49.0]; // [minLon, minLat, maxLon, maxLat]

const result = heightFromWidth(bboxParis, 800); console.log(result); // → { zoom: 11.458, height: 911.81 px } 解释:

要使巴黎 bbox 宽度达到 800 像素,您需要缩放 ≈ 11.5。 在缩放时,bbox 将大约有 912 像素高。 当这有帮助时 固定宽度的 Web 容器(高度可以增加的响应页面)。 必须与目标宽度匹配的景观静态地图图像。 打印列宽受限且高度可以流动的布局。查看更多www.mxwd.cc