ドキュメント / 地図ビュー / カメラ

カメラ ― 抽象化のいちばん難しいところ

MapConductor でもっとも難しく、もっとも価値があるのがカメラの統一です。position・zoom・bearing・tilt・visibleRegion からなる1つのカメラモデルを、10種類以上の地図エンジンで同じ意味に揃えています。同じ MapCameraPosition を渡せば、どのプロバイダでも同じ景色が出ます。

POSITION
lat / lng
ZOOM
0 – 22
BEARING
0 – 360°
TILT
−60 – 60°

01 · なぜ統一に意味があるのか

地図エンジンはそれぞれ別のカメラモデルを持っています。ズームで語るもの、カメラ高度で語るもの、地上の注視点を中心に回るもの、視点そのものを動かすもの。ネイティブSDKをそのまま使うと、プロバイダを差し替えた瞬間に「同じ数値なのに違う景色」になり、画面のロジックまで書き換えることになります。

MapConductor はこの差をアダプタ層に閉じ込め、アプリからは1つのカメラモデルだけを見せます。結果として、カメラ位置はプロバイダをまたいで持ち運べる値になります。サンプルアプリの Camera Sync ページでは、1つのカメラ位置を全プロバイダに流し込み、同じ地点・同じ縮尺・同じ傾きで並ぶことを実際に確認できます。

DIFFERENCE 01

ズームか、高度か

2Dタイル系はズームレベル、3D系はカメラ高度や視距離で縮尺を表します。単位も基準も違います。
DIFFERENCE 02

注視点か、視点か

地上の1点を中心に回る(オービット)モデルと、カメラ自身の位置と向きで語るモデルが混在します。
DIFFERENCE 03

傾きの許容範囲

最大傾斜角も、上向きに傾けられるかも、エンジンごとに異なります。負の傾きを持つものはひとつもありません。
図 · 1回の moveCameraTo が各エンジンで同じ視点になるまで
val camera = MapCameraPosition(
    position = geoPoint,
    zoom = 18.0,
    tilt = 70.0,
)
moveCameraTo(camera)
MapConductor
統一カメラモデル
position / zoom · bearing / tilt
各社 地図SDK
アダプター
各社 地図SDK
アダプター
アダプターがネイティブのカメラ表現へ翻訳します。ズーム基準・高度基準・上向き不可といった差はここで吸収されます。
ズーム基準のエンジン(Google Maps · Mapbox · MapLibre …)
高度基準のエンジン(ArcGIS · Cesium · MapKit …)
入口はひとつ。出口はエンジンごとに違う表現ですが、画面に出る景色は同じ地点・同じ縮尺・同じ傾きになります。
サンプル動画 · Camera Sync(1つのカメラ位置を全プロバイダに同期)

02 · 統一カメラモデル

カメラは MapCameraPosition ひとつで表します。プロバイダごとの型はなく、この値を作って moveCameraTo に渡すだけです。

プロパティ
説明
position
カメラが見ている地上の中心座標。
zoom
縮尺。Google Maps のズームレベルを基準に全プロバイダで正規化しています。
bearing
方位(真北からの回転角)。
tilt
傾き。0 が真上から、正が前方に伏せた斜め視点、負が水平線より上を見る仰角ビュー。
paddings
UI に隠れる領域。中心やフィット計算はこれを差し引いて行われます。
visibleRegion
実際に画面に映っている地上の範囲。矩形の bounds と四隅の座標を持ちます。

copy() で一部だけ差し替えられ、比較はズーム 0.01・傾き 0.01 の許容誤差付きで行われます。プロバイダから返ってくる値のわずかな揺れで「動いた」と誤判定しないためです。

プラットフォーム

03 · ズームレベル=高さの統一

ズームは、内部では「カメラの高さ」と同じものとして扱われます。2Dタイル系のズーム、3D系の高度・視距離を相互に変換できるコンバータが core にあり、どのプロバイダの値もいったん共通のズーム(Google Maps 基準)に直してから公開されます。

この基準は理屈だけで決めたものではありません。サンプルアプリのキャリブレーションツールで、同じズームを指定したときに実際に見えている範囲を各エンジンで実測し、面積が一致するように基準高度を校正しています。だから zoom 14 は、どのプロバイダでも同じ広さです。

図 · ズームレベルとスケールの目安(数値は画面の横幅がおよそ何kmにあたるか)
0
≈ 40,000 km
地球全体
全球が1画面に収まる。大陸の形だけが見える。
3
≈ 5,000 km
大陸
大陸と海の輪郭。国境はまだ意味を持たない。
6
≈ 600 km
国全体と主要都市の位置関係。
10
≈ 40 km
都市圏
市街地と幹線道路。地図アプリの初期表示によく使う。
14
≈ 2.5 km
市街
街路網が読める。店舗一覧のピンを置く目安。
17
≈ 300 m
街区
交差点・建物の形。徒歩ナビの標準。
20
≈ 40 m
敷地・建物
建物の入口やピンポイントの位置。
1段上げるごとに縮尺は2倍。表示の目安は赤道付近・画面幅 256px あたりの換算で、緯度によって実際の距離は変わります。
LATITUDE

同じズームでも、緯度で必要な高さが変わる

Web メルカトルでは高緯度ほど地図が拡大されるため、同じズームでも赤道付近と高緯度では必要なカメラ高度が数倍違います。統一ズーム ⇄ 高度の変換にはこの補正が含まれています。

ZOOM 10 · 赤道付近
≈ 5,200 m
ZOOM 10 · 北緯70度付近
≈ 1,700 m
NATIVE
ネイティブの値をこの目盛りに揃える
±0.0
Google Maps · Mapbox 2D — そのまま基準として採用。統一ズームはこの系列に合わせています。
+1.0
MapLibre · MapTiler · TomTom — ネイティブズームが1段小さいので +1.0 のオフセットを当てて揃えます。
m
MapKit · ArcGIS · Cesium · Google 3D — ズーム ⇄ 高度(視距離)を相互変換します。緯度と傾きも考慮されます。
ZOOM RANGE
0.0 – 22.0
ZOOM FACTOR
2.0
ZOOM 0 ALTITUDE
171,319,879 m
MAPLIBRE OFFSET
+1.0

2倍ごとに1段という関係と、基準となる zoom 0 の高度だけが定数です。緯度が高いほど同じズームでの地上距離が縮む点も変換に含まれているため、東京とホノルルで縮尺がずれることはありません。

04 · tilt < 0 の擬似サポート

tilt は −60〜60 度を取ります。正の値は見慣れた斜め見下ろしですが、負の値は水平線より上を見る仰角ビューです。地図エンジンには、これを直接表現できるものがひとつもありません。どのSDKも pitch は 0 以上しか受け付けません。

そこで MapConductor は、カメラの目の位置と向きを保ったまま、エンジンには正の傾きとして与える形に置き換えます。アプリから見れば負の tilt はそのまま使えて、取得したカメラ位置も負の tilt として返ってきます。

tilt = 0
真上から見下ろす平面図。地上の矩形がそのまま画面の矩形になります。
tilt > 0
前方に伏せた斜め視点。地上では手前が狭く奥が広い台形になります。
tilt < 0
水平線より上を向く仰角ビュー。空側を含む視点で、ネイティブには存在しません。
擬似表現のしくみ
01
カメラの目の位置と高さは動かさず、地上の注視点だけを bearing の方向へ前進させます。前進量はカメラ高度と傾き角から決まります。
02
エンジンには abs(tilt) の下向きピッチとして渡します。目の位置・向きが一致するため、見えている絵は仰角ビューと同じになります。MapLibre 系のエンジンでは、見た目を合わせるために前進量とズームに固定の補正係数を掛けています。
03
カメラ位置を読み出すときは同じ計算を逆にたどり、前進した注視点とズーム補正を打ち消して、論理的な負の tilt と元の中心座標に戻します。往復しても値が変わりません。
負 tilt の対応状況
Android · 全プロバイダ
OK
iOS · 全プロバイダ
OK
React · Google Maps 2D 以外
OK
React · Google Maps 2D
N/A

React の Google Maps 2D(JavaScript API のラスタ地図)だけは対応外です。傾きがブラウザとタイル側の条件に依存し、擬似表現の前提となる高度と向きを制御できないためです。Google Maps 3D(Map3DElement)では負 tilt が使えます。

TiltMapPageViewModel.kt
// −60〜60 のスライダーをそのまま渡すだけ。負の値でも同じ呼び出し
currentPosition = currentPosition.copy(tilt = tilt)
mapViewState.value?.moveCameraTo(currentPosition)
サンプル動画 · Tilt(−60〜60 を連続で動かす)

05 · VisibleRegion ― 実際に見えている範囲

カメラを傾けると、画面の四角形は地上では台形になります。南西と北東だけの矩形(bounds)では、手前と奥の広さの違いを表せません。VisibleRegion は矩形に加えて画面四隅の地上座標を持ち、傾いたカメラでも「本当に映っている範囲」を扱えるようにします。

near は画面の下辺、つまりカメラに近い側、far は上辺で遠い側です。left / right は画面基準なので、bearing で地図を回しても意味は変わりません。この4点が揃っているおかげで、傾いた地図でもデータ取得範囲や出典表記の判定を正しく行えます。

farLeft
farRight
nearLeft
nearRight
画面の矩形 → 地上では台形
奥(far)ほど広く、手前(near)ほど狭くなります。bounds はこの台形を囲む矩形です。
bounds
台形を囲う南西・北東の矩形。ざっくりした範囲判定や既存APIとの互換に使います。
nearLeft / nearRight
画面下辺の左右の地上座標。カメラに近い側。
farLeft / farRight
画面上辺の左右の地上座標。遠い側で、傾けるほど大きく広がります。
使いどころ
表示範囲のデータ取得、fitBounds、タイル出典表記(attributionRules)の切り替えなど。
VisibleRegionViewModel.kt
mapViewState.cameraPosition.visibleRegion?.let { region ->
    val sw = region.bounds.southWest
    val ne = region.bounds.northEast
    // 傾いた画面の四隅。台形として扱える
    val corners = listOf(region.nearLeft, region.nearRight, region.farLeft, region.farRight)
}

関連ページ