ドキュメント / コア / 投影とズーム

投影とズーム

丸い地球を平らな画面に落とすのが投影、どこまで寄っているかを表すのがズームです。プロバイダごとにどちらの定義もばらつきますが、MapConductor は同じ定数と同じ式を 3 プラットフォームで共有し、公開される値をひとつの尺度にそろえます。カメラの使い方はカメラのページを、ここではその下にある数値を扱います。

ELLIPSOID
WGS84
PROJECTIONS
WebMercator · WGS84
ZOOM RANGE
0 – 22
プラットフォーム

01 · 地球の定数

投影・地理計算・タイル計算はすべて Earth の定数を参照します。

定数
説明
RADIUS_METERS
6 378 137.0
WGS84 の長半径(赤道半径)。
CIRCUMFERENCE_METERS
2πa
赤道の全周。タイルの分解能計算に使います。
FLATTENING
1 / 298.257223563
扁平率。楕円体モデルの測地線計算で使います。
SEMI_MINOR_AXIS_METERS
a(1 − f)
短半径(極半径)。
ECCENTRICITY_SQUARED
f(2 − f)
第一離心率の 2 乗。
WEB_MERCATOR_MAX_EXTENT_METERS
πa ≈ 20 037 508.34
Web メルカトルの座標が取りうる最大値。投影後の x・y はこの範囲に収まります。

02 · 投影

投影の型は project と unproject の 2 つだけを持ちます。座標を平面の座標(Offset)に落とし、また戻すという対称な操作です。実装は 2 つ用意されています。

WebMercator

メートル単位の Web メルカトル

EPSG:3857 と同じ、±20,037,508.34 m の平面に落とします。タイル境界の計算や、画面上の距離をメートルへ換算する処理で使います。

WGS84

256 ピクセルのタイル座標

ズーム 0 の 1 枚のタイル(256×256)を単位とする座標に落とします。タイル画像を生成するレンダラーが使う座標系です。

com.mapconductor.core.projection
interface ProjectionInterface {
    fun project(position: GeoPointInterface): Offset
    fun unproject(point: Offset): GeoPointInterface
}

val meters = WebMercator.project(tokyo)   // x, y はメートル
val tile = WGS84.project(tokyo)           // 0..256 のタイル座標

画面上のピクセルと座標の相互変換が必要なときは、投影ではなく MapViewHolder の toScreenOffset / fromScreenOffset を使ってください。実際に表示されているカメラの傾きや回転が反映されます。

03 · ズームと高度の変換

2D タイル系のプロバイダはズームレベルで、3D 系はカメラ高度や視距離で縮尺を表します。MapConductor はこの 2 つを相互変換し、アプリにはつねに同じズーム尺度で見せます。変換はプロバイダごとに 1 クラス、共通の抽象クラスを継承する形で実装されています。

変換には緯度と傾きが入ります。Web メルカトルは高緯度ほど地図を拡大するため、同じズームでも赤道と高緯度では必要なカメラ高度が数倍違うからです。

図 · 同じズーム値で必要なカメラ高度(赤道を 1 とした比)
0° · 赤道
×1.00
30° · カイロ
×0.87
35° · 東京
×0.82
同じ zoom 14 でも、赤道より 2 割ほど低い位置から見ていることになります。
60° · オスロ
×0.50
70° · トロムソ
×0.34
赤道との差はおよそ 3 倍。緯度を無視して高度だけを合わせると、見えている範囲がここまでずれます。
AbstractZoomAltitudeConverter · Kotlin
abstract class AbstractZoomAltitudeConverter(
    protected val zoom0Altitude: Double,
) {
    abstract fun zoomLevelToAltitude(zoomLevel: Double, latitude: Double, tilt: Double): Double

    abstract fun altitudeToZoomLevel(altitude: Double, latitude: Double, tilt: Double): Double
}

コンバータは各プロバイダのパッケージが持ちます(ZoomAltitudeConverter)。アプリが直接呼ぶ必要はありませんが、独自の 3D 表現を組み立てるときには使えます。

動画で見せる実装 · Android + MapLibreKotlin · Jetpack Compose
// 同じカメラ位置を 2 つの state に与える
val shared = MapCameraPosition(
    position = GeoPoint.fromLatLong(35.6812, 139.7671),
    zoom = 14.0,
)
val maplibre = rememberMapLibreMapViewState(cameraPosition = shared)
val googlemaps = rememberGoogleMapViewState(cameraPosition = shared)

// 片方を動かしたら、もう片方へ同じ値を渡す
Row {
    MapLibreMapView(
        state = maplibre,
        modifier = Modifier.weight(1f),
        onCameraMoveEnd = { googlemaps.moveCameraTo(it) },
    )
    GoogleMapView(
        state = googlemaps,
        modifier = Modifier.weight(1f),
        onCameraMoveEnd = { maplibre.moveCameraTo(it) },
    )
}
サンプル動画 · 同じズーム値をプロバイダ間で見比べる
動画 未撮影同じ zoom を与えたままプロバイダを切り替え、実際の縮尺がどれだけずれるかを見せる。

04 · 境界値

変換に使われる定数です。極付近や真上からの見下ろしで式が発散しないよう、cos の下限も定数として決めてあります。

DEFAULT_ZOOM0_ALTITUDE
171,319,879 m
ZOOM_FACTOR
2.0
MIN / MAX_ZOOM_LEVEL
0.0 / 22.0
MIN / MAX_ALTITUDE
100 / 50,000,000 m
MIN_COS_LAT
0.01
MIN_COS_TILT
0.05
WEB_MERCATOR_INITIAL_MPP_256
156,543.033928
TILE SIZE
256 px

zoom 0 の基準高度は理屈だけで決めた値ではありません。同じズームを指定したときに実際に見えている範囲を各エンジンで実測し、面積が一致するよう校正した結果です。ズーム 1 段で高度が半分になる(ZOOM_FACTOR = 2)という関係と、この基準値だけが定数です。

関連ページ