先日公開した「Google Maps エリアエディタ」を開発する中で、地図上の「ピクセル座標(X, Y)」から「緯度経度(LatLng)」を自力で逆算しなければならない場面に直面しました。
その実装のために調べてわかった「Googleマップのズームレベルの本当の意味」と、最近のAPIに導入された「フラクショナルズーム(小数のズーム)」を私があえて無効化した理由について、備忘録としてまとめておきます。
1. ズームレベルの値は「何」を表しているのか?
マウスカーソルのピクセル座標から緯度経度を逆算しようとした際、「そもそも map.getZoom() で取得できるズームレベル(18とか19という数字)が、数学的に何を意味しているのか?」を理解していないことに気づきました。
海外の技術フォーラムなどを漁って調べてみたところ、以下のような仕様であることがわかりました。
- Googleマップは、ズームレベル「0」のとき、世界全体を 256 × 256ピクセル の1枚のタイルとして表現する。
- ズームレベルが「1」上がるごとに、縦横のサイズがそれぞれ2倍になる。
- つまり、任意のズームレベルにおけるスケールは 2zoom で計算できる。
これを踏まえ、現在の画面の境界(Bounds)と組み合わせてピクセル座標を逆算する処理を書きました。整数乗の計算なので、JavaScriptのビットシフト演算(1 << zoom)を使うことで非常にシンプルに記述できます。
function toLatLng(map, x, y) {
const projection = map.getProjection();
const bounds = map.getBounds();
const ne = projection.fromLatLngToPoint(bounds.getNorthEast());
const sw = projection.fromLatLngToPoint(bounds.getSouthWest());
// ズームレベルからスケール比率を算出(2のzoom乗)
const scale = 1 << map.getZoom();
// ピクセル座標をスケールで割り、緯度経度に変換
return projection.fromPointToLatLng(new google.maps.Point(x / scale + sw.x, y / scale + ne.y));
}
2. 新機能「フラクショナルズーム」の壁
さて、ここで一つの問題が浮上します。Google Maps JavaScript APIの比較的最近のアップデートで、「フラクショナルズーム(Fractional Zoom)」という機能が導入されました。
これは、スマホのピンチイン・アウトやMacのトラックパッド操作などで、ズームレベルが「18.1」や「18.5」といった小数(Fractional)で滑らかに変化する機能です。
ズームが小数になるということは、先ほどのビットシフト演算(1 << zoom)は使えなくなります。
もちろん、計算式を Math.pow(2, map.getZoom()) に書き換えれば、数学的には小数のズームレベルでもピクセル座標の逆算は対応可能です。
しかし、私はこのエリアエディタにおいて、フラクショナルズームをあえて無効にする(isFractionalZoomEnabled: false)という決断を下しました。
3. あえて「無効」にした本当の理由(パフォーマンス問題)
数学的な計算が対応できても、フラクショナルズームを有効にすることには「イベントの連続発火」という大きな罠が潜んでいました。
Googleの公式ドキュメントにもパフォーマンスに関する注意書きがありますが、フラクショナルズームが有効な状態でユーザーがピンチ操作等を行うと、ズームが「18.0 → 18.01 → 18.02…」と変化するたびに、極めて短いスパンでイベント(zoom_changed や bounds_changed 等)が大量に発火し続けます。
今回作成したエリアエディタは、以下のような重い処理を抱えています。
- 矩形の四隅の座標のリアルタイムな再計算(球面幾何学のベクトル演算)
- 複数の
AdvancedMarkerElement(操作ハンドル)のDOM再配置 - 図形(
Polygon)の再描画
これらの処理が高頻度で呼び出されると、場合によってはブラウザのレンダリングが追いつかず、激しいカクつきやフリーズを引き起こしてしまいそうです。
「滑らかなズーム」というUXを取るか、「エディタとしての確実な動作とパフォーマンス」を取るかを天秤にかけた結果、今回は後者を優先し、ズームを強制的に整数ステップに固定することでイベントの暴走を防ぐという設計を選択しました。
まとめ
- Googleマップのスケール計算は 2zoom で成り立っている。
- フラクショナルズームは滑らかだが、イベントの過剰発火を引き起こす。
- カスタムUIや重いDOM操作を行う場合は、あえて
isFractionalZoomEnabled: falseで無効化するのも有効なパフォーマンス最適化の手段。
新しいAPIの機能をとりあえず全部ONにするのではなく、公式ドキュメントの警告を読み解き、自分のアプリケーションの特性に合わせて取捨選択することの重要性を改めて実感した開発でした。