
本文详解如何通过经纬度和缩放级别精准定位 mbtiles 文件中的对应瓦片,并重点解决因 tms 与 web mercator 坐标系差异导致的 tile_y 计算错误问题。
本文详解如何通过经纬度和缩放级别精准定位 mbtiles 文件中的对应瓦片,并重点解决因 tms 与 web mercator 坐标系差异导致的 tile_y 计算错误问题。
在使用 MBTiles 存储矢量地图数据(如 France 国家级提取)时,常需验证某地理坐标(如巴黎:48.8669846°N, 2.3470999°E)是否被包含在指定缩放级别(如 zoom=12)的瓦片中。核心逻辑分为两步:坐标转瓦片索引(x/y/z)→ 查询 MBTiles SQLite 数据库。但实践中,测试常返回 false——根本原因在于 MBTiles 规范采用 TMS(Tile Map Service)坐标系,而标准 Web Mercator(如 XYZ/Slippy Map)的 y 轴方向相反。
标准 Web Mercator(如 Mapbox、OpenStreetMap)定义:
-
tile_y = floor((0.5 − log(tan(π/4 + φ/2)) / π) × 2^z) - y 从上到下递增(北纬最高处 y=0)
而 TMS(MBTiles 默认)要求:
- y 从下到上递增(赤道附近 y ≈ 2^(z−1),南纬最低处 y = 2^z − 1)
- 因此必须对计算出的
tileY进行翻转:int tmsTileY = (1 << zoom) - 1 - tileY;
修正后的完整工具类如下:
public class TilesUtils {
public static int longitudeToTileX(double longitude, int zoom) {
double tileX = (longitude + 180.0) / 360.0 * (1 << zoom);
return (int) Math.floor(tileX);
}
public static int latitudeToTileY(double latitude, int zoom) {
double sinLat = Math.sin(Math.toRadians(latitude));
double tileY = (0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI)) * (1 << zoom);
return (int) Math.floor(tileY);
}
// ✅ 关键修正:转换为 TMS 兼容的 tile_row
public static int toTmsTileY(int webMercatorY, int zoom) {
return (1 << zoom) - 1 - webMercatorY;
}
}单元测试应同步更新:
@Test
void should_check_if_Paris_is_included_in_mbtiles() {
int zoom = 12;
double latitude = 48.8669846;
double longitude = 2.3470999;
int tileX = TilesUtils.longitudeToTileX(longitude, zoom);
int webMercatorY = TilesUtils.latitudeToTileY(latitude, zoom);
int tileY = TilesUtils.toTmsTileY(webMercatorY, zoom); // ← 必须调用此转换!
assertTrue(checkTilesIsIncluded(tileX, tileY, zoom, "/path/to/france.mbtiles"));
}数据库查询部分保持不变,但需确保 tile_column 和 tile_row 字段严格匹配 TMS 值:
private boolean checkTilesIsIncluded(int tileX, int tileY, int zoom, String mbTilesFilePath) {
String url = "jdbc:sqlite:" + mbTilesFilePath;
try (var conn = DriverManager.getConnection(url)) {
String sql = "SELECT 1 FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ?";
try (var ps = conn.prepareStatement(sql)) {
ps.setInt(1, zoom);
ps.setInt(2, tileX);
ps.setInt(3, tileY);
try (var rs = ps.executeQuery()) {
return rs.next(); // 存在即返回 true
}
}
} catch (SQLException e) {
throw new RuntimeException("Failed to query MBTiles", e);
}
}⚠️ 注意事项:
- 不要混淆
tile_row(TMS)与y(XYZ)——二者数值互为镜像; - MBTiles 的
zoom_level、tile_column、tile_row均为整数,且tile_column∈ [0, 2^z),tile_row∈ [0, 2^z); - 若仍查不到,请确认 MBTiles 文件确实包含该区域(可用
mbview或tippecanoe验证); - 对于矢量 MBTiles,即使瓦片存在,也需进一步解析
tile_data(Protocol Buffer 格式)判断几何是否覆盖该点——本文仅验证“瓦片是否存在”。
掌握这一坐标系转换是高效操作 MBTiles 的基础。正确应用 y → (2^z − 1 − y) 后,你的地理查询将稳定可靠。

















