投影とズーム
丸い地球を平らな画面に落とすのが投影、どこまで寄っているかを表すのがズームです。プロバイダごとにどちらの定義もばらつきますが、MapConductor は同じ定数と同じ式を 3 プラットフォームで共有し、公開される値をひとつの尺度にそろえます。カメラの使い方はカメラのページを、ここではその下にある数値を扱います。
01 · 地球の定数
投影・地理計算・タイル計算はすべて Earth の定数を参照します。
02 · 投影
投影の型は project と unproject の 2 つだけを持ちます。座標を平面の座標(Offset)に落とし、また戻すという対称な操作です。実装は 2 つ用意されています。
メートル単位の Web メルカトル
EPSG:3857 と同じ、±20,037,508.34 m の平面に落とします。タイル境界の計算や、画面上の距離をメートルへ換算する処理で使います。
256 ピクセルのタイル座標
ズーム 0 の 1 枚のタイル(256×256)を単位とする座標に落とします。タイル画像を生成するレンダラーが使う座標系です。
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 のタイル座標// iOS では投影はタイル・空間インデックス側の内部実装として持ちます。 // アプリからの座標 ⇄ 画面の変換は MapViewHolder を使います。 let offset = holder.toScreenOffset(position: tokyo) let point = holder.fromScreenOffsetSync(offset: tapLocation)
import { WebMercator, WGS84, Earth } from '@mapconductor/js-sdk-core';
const meters = WebMercator.project(tokyo); // { x, y } はメートル
const back = WebMercator.unproject(meters);
const tile = WGS84.project(tokyo); // 0..256 のタイル座標画面上のピクセルと座標の相互変換が必要なときは、投影ではなく MapViewHolder の toScreenOffset / fromScreenOffset を使ってください。実際に表示されているカメラの傾きや回転が反映されます。
03 · ズームと高度の変換
2D タイル系のプロバイダはズームレベルで、3D 系はカメラ高度や視距離で縮尺を表します。MapConductor はこの 2 つを相互変換し、アプリにはつねに同じズーム尺度で見せます。変換はプロバイダごとに 1 クラス、共通の抽象クラスを継承する形で実装されています。
変換には緯度と傾きが入ります。Web メルカトルは高緯度ほど地図を拡大するため、同じズームでも赤道と高緯度では必要なカメラ高度が数倍違うからです。
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
}public protocol ZoomAltitudeConverterProtocol {
var zoom0Altitude: Double { get }
func zoomLevelToAltitude(zoomLevel: Double, latitude: Double, tilt: Double) -> Double
func altitudeToZoomLevel(altitude: Double, latitude: Double, tilt: Double) -> Double
}export abstract class AbstractZoomAltitudeConverter {
constructor(protected readonly zoom0Altitude: number =
AbstractZoomAltitudeConverter.DEFAULT_ZOOM0_ALTITUDE) {}
abstract zoomLevelToAltitude(params: { zoomLevel: number; latitude: number; tilt: number }): number;
abstract altitudeToZoomLevel(params: { altitude: number; latitude: number; tilt: number }): number;
}コンバータは各プロバイダのパッケージが持ちます(ZoomAltitudeConverter)。アプリが直接呼ぶ必要はありませんが、独自の 3D 表現を組み立てるときには使えます。
// 同じカメラ位置を 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) },
)
}04 · 境界値
変換に使われる定数です。極付近や真上からの見下ろしで式が発散しないよう、cos の下限も定数として決めてあります。
zoom 0 の基準高度は理屈だけで決めた値ではありません。同じズームを指定したときに実際に見えている範囲を各エンジンで実測し、面積が一致するよう校正した結果です。ズーム 1 段で高度が半分になる(ZOOM_FACTOR = 2)という関係と、この基準値だけが定数です。