第4章: Webメルカトルとタイル座標

Slippy Mapタイル座標系の球面上可視化

はじめに

メルカトル投影とSlippy Mapタイル座標はGIS技術者にとって既知の概念ですが、2D地図では正方形タイルを平面に並べるだけなのに対し、3Dでは球面上の曲線として描画する必要があります。

この章では、平面で見慣れたタイルグリッドが球面ではどう見えるかを3Dで観察できます。ズームレベルを切り替えると、タイルの分割パターンが球面上でどう変化するかを直感的に理解できます。

この章で学ぶこと

  • メルカトル投影法の数学的な仕組み —— 変換式と逆変換式の導出
  • Webメルカトル(EPSG:3857)の特徴と制限 —— 厳密メルカトルとの違い
  • 有効緯度範囲(±85.05°)の数学的由来 —— 正方形タイルを成立させるための制約
  • Slippy Map Tiles(XYZタイル)の仕組み —— z/x/y座標の変換アルゴリズム
  • 経度緯度 ↔ タイル座標の変換 —— X座標の導出とY座標のメルカトル経由の導出
  • タイルのバウンディングボックス計算とタイルURL規約 —— 実用的なタイル操作

ズームレベル

タイル数: 16 (4 x 4)

投影法とは

地球は3次元の楕円体ですが、地図タイルは2次元の正方形画像です。 3次元の曲面を2次元の平面に変換する操作を地図投影(map projection)と呼びます。 投影法には多くの種類がありますが、Webの地図タイルシステムではほぼ例外なく メルカトル投影法が使われています。

メルカトル投影法の数学

メルカトル投影法は、1569年にゲラルドゥス・メルカトルが考案した正角円筒図法です。 「正角」とは、地図上の任意の点で局所的な角度が保存される(局所的に小さな図形の形状が保存される)ことを意味します。

経度 lam(ラジアン)、緯度 phi(ラジアン)をメルカトル座標 (x, y) に変換する式:

x = a * lam

y = a * ln( tan( pi/4 + phi/2 ) )

ここで a はWGS84の長半径(6,378,137m)です。 経度方向はそのまま線形に対応しますが、 緯度方向は非線形変換を使います。 この変換により高緯度ほど引き伸ばされ、 グリーンランドがアフリカより大きく見えるという歪みが生じます。

lonLatToMercator
export function lonLatToMercator(
  lon: number, lat: number
): MercatorCoord {
  const lam = degToRad(lon);
  const phi = degToRad(clampLatitude(lat));
  return {
    x: lam * WGS84.a,
    y: WGS84.a * Math.log(
      Math.tan(Math.PI/4 + phi/2)
    )
  };
}

逆変換

逆変換の緯度計算は、以下の等式に基づいています:

phi = 2 * atan(exp(y/a)) - pi/2
mercatorToLonLat
export function mercatorToLonLat(
  mx: number, my: number
): { lon: number; lat: number } {
  const lam = mx / WGS84.a;
  const phi = 2 * Math.atan(
    Math.exp(my / WGS84.a)
  ) - Math.PI / 2;
  return {
    lon: radToDeg(lam),
    lat: radToDeg(phi)
  };
}

なぜ±85.05°が限界?

メルカトルY座標の式 y = ln(tan(pi/4 + phi/2)) において、 phi → ±90° のとき y → ±∞ となります。極点では投影が無限遠に発散するのです。

タイル地図システムでは、地図全体を正方形に収める必要があります。 メルカトル座標のX範囲は [-pi*a, pi*a] です。 Y方向も同じ範囲に収まる緯度を求めると:

pi*a = a * ln( tan(pi/4 + phi_max/2) )

phi_max = 2 * atan(exp(pi)) - pi/2

        = 85.0511°

つまり、±85.05°はメルカトル地図が正方形になる境界の緯度です。 南極大陸の内陸部は描画対象外になりますが、実用上の影響は限定的です。

clampLatitude
export const MAX_MERCATOR_LATITUDE =
  85.051_128_78;

export function clampLatitude(
  lat: number
): number {
  return Math.max(
    -MAX_MERCATOR_LATITUDE,
    Math.min(MAX_MERCATOR_LATITUDE, lat)
  );
}

Column: 厳密メルカトルとWebメルカトルの違い

厳密なメルカトル投影法では、楕円体の離心率 e を考慮した補正項が入ります。 しかし、Webメルカトル(EPSG:3857)は地球を球として扱い、この補正を省略しています。 これはGoogleが2005年にGoogle Mapsを開発した際の設計判断です。 EPSG(測地系コード管理機関)は当初、この「不正確な」投影法にコード付与を拒否していました。 最大で約0.33%の誤差がありますが、タイル地図の用途では十分な精度です。

正規化メルカトルY

タイル座標系での緯度の補間(第5章で使用)のために、 WGS84スケーリングなしの「正規化メルカトルY」関数を用意しています。 lonLatToMercator との違いは、a(赤道半径)を掛けないことです。

latToMercatorY
export function latToMercatorY(
  lat: number
): number {
  const phi = degToRad(clampLatitude(lat));
  return Math.log(
    Math.tan(Math.PI/4 + phi/2)
  );
}

タイルの球面メッシュ生成(第5章)では、「タイル範囲内で緯度をメルカトルY空間で線形補間する」必要があり、 このときスケーリングは不要です。 タイル画像はメルカトル投影されており、タイル画像のピクセルY座標はメルカトルYに対して線形です。 もし緯度空間で等間隔に分割すると、高緯度ほどテクスチャが引き伸ばされ、タイル画像との対応がずれます。

タイル座標系(Slippy Map Tiles)

Web地図で広く使われるタイル座標系は、Slippy Map Tilesと呼ばれ、 OpenStreetMapが標準化しました。基本的な仕組みは以下の通りです。

  • 地球全体のメルカトル投影図を正方形として扱う
  • ズームレベル z で、この正方形を 2^z x 2^z のグリッドに分割する
  • 各グリッドセルに (x, y, z) の座標を付ける
  • 各セルに対応する256x256ピクセルの画像を配信する

タイル座標は左上が原点(0,0)で、x は右方向(東)、y は下方向(南)に増加します。

ズームレベルタイル数赤道での1タイル幅
0140,075 km
1420,038 km
42562,505 km
865,536156 km
1216,777,2169.8 km
164,294,967,296610 m

経度緯度からタイル座標への変換

X座標の計算は直感的です。経度を0〜360の範囲に正規化し、 2^z 個のタイルに均等に振り分けます。

x = floor( (lon + 180) / 360 * 2^z )

Y座標はメルカトル投影を経由するため、やや複雑です。 Y座標は北が0であることに注意してください。

y = floor( (1 - ln(tan(phi) + sec(phi))/pi) / 2 * 2^z )
lonLatToTile
export function lonLatToTile(
  lon: number, lat: number, zoom: number
): TileCoord {
  const latRad = degToRad(
    clampLatitude(lat)
  );
  const n = 2 ** zoom;
  const x = Math.floor(
    ((lon + 180) / 360) * n
  );
  const y = Math.floor(
    ((1 - Math.log(
      Math.tan(latRad)
      + 1/Math.cos(latRad)
    ) / Math.PI) / 2) * n
  );
  return {
    x: Math.max(0, Math.min(n-1, x)),
    y: Math.max(0, Math.min(n-1, y)),
    z: zoom
  };
}

タイル座標から経度緯度への逆変換

タイル (x, y, z) の左上角の経度緯度を返します。 右下角を得るには (x+1, y+1, z) を渡します。 緯度の逆変換で使われる Math.sinh は、 sinh(x) = (exp(x) - exp(-x))/2 であり、メルカトルY→緯度の逆変換を簡潔に表現しています。

tileToLonLat
export function tileToLonLat(
  x: number, y: number, z: number
): { lon: number; lat: number } {
  const n = 2 ** z;
  const lon = (x / n) * 360 - 180;
  const latRad = Math.atan(
    Math.sinh(
      Math.PI * (1 - (2*y) / n)
    )
  );
  return { lon, lat: radToDeg(latRad) };
}

タイルのバウンディングボックス

タイルの地理的な範囲は、tileToLonLat を2回呼ぶことで求められます。 この TileBounds は後続の章で頻繁に使われます: タイルの球面メッシュ生成(第5章)、MVTタイルのジオメトリ変換など。

tileBounds
export function tileBounds(
  x: number, y: number, z: number
): TileBounds {
  const nw = tileToLonLat(x, y, z);
  const se = tileToLonLat(x+1, y+1, z);
  return {
    west: nw.lon,  east: se.lon,
    north: nw.lat, south: se.lat
  };
}

タイルURL規約

タイルサーバーは、z/x/y のパスでタイル画像を配信する規約に従います。 TileUrlGenerator 型を定義し、関数として切り出すことで、タイルソースの切り替えが容易になっています。

タイルURL
export type TileUrlGenerator = (
  z: number, x: number, y: number
) => string;

// OpenStreetMapタイルURL
export const osmTileUrl:
  TileUrlGenerator = (z, x, y) =>
  `https://tile.openstreetmap.org/$
    {z}/${ x}/${ y}.png`;

// 国土地理院標準タイルURL
export const gsiTileUrl:
  TileUrlGenerator = (z, x, y) =>
  `https://cyberjapandata.gsi.go.jp/
    xyz/std/${ z}/${ x}/${ y}.png`;

Column: TMS vs XYZ

タイル座標のY軸の向きには2つの規約があります。 XYZ(Slippy Map Tiles)ではY=0が北端で、本書で使用しています。 TMS(Tile Map Service)ではY=0が南端です(Y_TMS = 2^z - 1 - Y_XYZ)。 OSM、Google Maps、Mapbox等はXYZを採用しており、Web地図ではXYZが事実上の標準です。

球面上へのタイル境界描画

球面上にタイルの境界線を描画する際、辺を直線で結ぶと球面の曲率でずれてしまうため、 各辺を20点サンプリングして曲線として近似しています。

タイル境界の描画
const bounds = tileBounds(tx, ty, z);

// 北辺: west→eastを20分割して点を生成
for (
  let i = 0; i <= EDGE_SAMPLES; i++
) {
  const lon = bounds.west
    + (bounds.east - bounds.west)
    * (i / EDGE_SAMPLES);
  points.push(
    geoToSceneVec3(
      bounds.north, lon, TILE_ALTITUDE
    )
  );
}

色分けとZファイティング回避

隣接タイルの区別のため、 (tx + ty) % 8 で8色のパレットから色を割り当てています。 チェッカーボード的に色が変わるため、タイルの境界が視覚的に明確です。

タイルラインは楕円体の表面ではなく、 50km上空(TILE_ALTITUDE = 50000)に描画しています。 楕円体表面と完全に同一の位置に配置すると、 深度値が同じになり描画順序が不安定になるZファイティングが発生するためです。

タイルラベル

z ≤ 3のときは各タイルの中心に「z/x/y」のラベルが表示されます。 CanvasテクスチャでSprite(常にカメラに正面を向くビルボード)を生成し、 タイルの中心上空30kmに配置しています。 z ≥ 4ではタイル数が多くなりすぎるためラベルは非表示にしています。

まとめ

この章では、Webメルカトル投影法とタイル座標系を実装しました。

  • メルカトル変換: x = a*lam, y = a*ln(tan(pi/4 + phi/2)) とその逆変換
  • 有効緯度範囲: ±85.05°(メルカトル地図が正方形になる境界)
  • 正規化メルカトルY: タイルのテクスチャマッピング用のスケーリングなし版
  • Slippy Map Tiles: ズームレベル z で地球を 2^z x 2^z に分割
  • lonLatToTile / tileToLonLat: メルカトル投影を経由した双方向変換
  • tileBounds: タイルの地理的範囲(west/east/south/north)

次章では、このタイル座標から球面メッシュを生成し、タイル画像をテクスチャとして貼り付ける処理を実装します。