0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

地図に大量のピンを置くと重い・見えない問題を、Leaflet.markercluster で解決する

0
Posted at

何の話か

地図サービスを作っていると、必ずぶつかる問題があります。

ピンが多すぎて、地図が見えない。

自分が運営しているバイク情報サイトの地図機能でも、都心を開くと駐車場・ショップ・ガソリンスタンドのピンが200件以上重なって、地図が「ピンの絨毯」になっていました。1つ1つのピンをタップするどころか、下の地図がどこなのかも分からない。

これを、いわゆる「Googleマップでよくあるアレ」——ズームアウトすると数字のクラスタにまとまり、ズームインすると個別ピンに割れる——で解決した話です。使うのは Leaflet 定番プラグインの Leaflet.markercluster

実装自体は素直ですが、**「本番で事故らないための小さな判断」**がいくつかあったので、そこを中心にまとめます。


Before / After

  • Before: 全ピンを L.marker で個別に地図へ addTo。ズームアウトすると重なって潰れる。描画も重い。
  • After: 全ピンを1つの markerClusterGroup に載せる。ズームレベルに応じて自動でクラスタ化 / 個別化。

image.png


最小実装

1. 読み込み

markercluster は CSS 2本 + JS 1本。Leaflet 本体の後・自分の地図スクリプトの前に置きます。

<!-- Leaflet 本体 -->
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>

<!-- markercluster(Leaflet の後・自分の map.js の前) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.Default.css" />
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>

<!-- 自分の地図スクリプト -->
<script src="/js/map.js"></script>

読み込み順を間違えると L.markerClusterGroup is not a function で落ちるので、この順序が肝です。

2. グループを差し替えるだけ

これまで L.layerGroup() に marker を addTo していたところを、L.markerClusterGroup() に変えるだけです。

// Before
const layer = L.layerGroup().addTo(map);
markers.forEach((m) => L.marker([m.lat, m.lng]).addTo(layer));

// After
const layer = L.markerClusterGroup();
markers.forEach((m) => L.marker([m.lat, m.lng]).addTo(layer));
map.addLayer(layer);

これだけで、ズームアウト時は数字クラスタ、ズームインで個別ピンに割れるようになります。クラスタをクリックすれば自動でズームイン(zoomToBounds)、個別ピンのクリックは従来どおりポップアップ、という挙動も標準で付いてきます。


本番で効いた「小さな判断」

ここからが本題です。ライブラリを入れるだけなら5分ですが、運用中のサービスに入れるときに効いた判断が3つあります。

判断1: 種類ごとにクラスタしない(種類横断で1つにまとめる)

地図には複数の種類のピンがありました(駐車場・ショップ・GS・…)。最初は「種類ごとにクラスタグループを分ける」ことも考えましたが、やめました。

理由: 都心では種類の違うピン同士が近接しているため、種類別にクラスタすると、同じ場所に複数の数字バッジが重なって、結局ごちゃつく。

なので 全種類を1つの markerClusterGroup にまとめる(Googleマップと同じく種類横断でまとまる)方が、体験がきれいでした。個別ピンに割れたときの色(=種類の区別)はマーカー側で維持されるので、種類が分からなくなることもありません。

クラスタは「密度を減らすため」の機能。種類の区別は個別ピンの見た目に任せる、と割り切るのが正解でした。

判断2: CDN が落ちても地図は動かす(フォールバック)

markercluster を CDN から読み込む以上、CDN が一時的に落ちる / ブロックされる可能性があります。そのとき地図ごと真っ白になるのは最悪です。

なので、L.markerClusterGroup が読めているかを確認し、無ければ従来の L.layerGroup にフォールバックするようにしました。

const makeLayer = () =>
  typeof L.markerClusterGroup === "function"
    ? L.markerClusterGroup()
    : L.layerGroup(); // CDN 失敗時は素の layerGroup(クラスタ無しに劣化するだけ)

const layer = makeLayer();

これで最悪の場合も「クラスタ無しの地図」に劣化するだけで済み、地図が死ぬことはありません。新しい依存を足すときは、それが無くても本体が動く設計にしておくと安心です。

判断3: フィルタ切替時に再クラスタする

「駐車場だけ表示」のようなフィルタがある場合、フィルタ後に表示中のピンだけで再クラスタしないと、消したはずのピンがクラスタの数に残ってしまいます。

やることは単純で、フィルタ時に clearLayers() してから、有効なマーカーだけを addLayer し直すだけです。

function applyFilter(activeTypes) {
  layer.clearLayers();
  markers
    .filter((m) => activeTypes.includes(m.type))
    .forEach((m) => layer.addLayer(L.marker([m.lat, m.lng])));
}

クラスタの数字が常に「今表示されているピンの数」と一致するので、ユーザーが混乱しません。


モバイルでの副次効果

余談ですが、このクラスタ化はモバイルで特に効きました。狭い画面で200個のピンが重なると操作不能でしたが、クラスタ化で「まず数字 → タップで拡大 → 個別ピン」という自然な段階が生まれ、片手でも扱えるようになりました。

地図UIをモバイルで作るなら、クラスタリングは「あったら便利」ではなく「ほぼ必須」だと感じます。


まとめ

  • 大量ピンの地図は、L.layerGroupL.markerClusterGroup に差し替えるだけでクラスタ化できる。
  • 読み込み順(Leaflet 本体 → markercluster → 自分のスクリプト)に注意。
  • 運用サービスに入れるなら、種類横断で1クラスタ / CDN 失敗時のフォールバック / フィルタ後の再クラスタ の3点を押さえると事故らない。

「ピンが多すぎて地図が見えない」で悩んでいる人の参考になれば。


この実装は、バイクの中古車検索・ツーリング情報サイト MotoHub の地図機能で使っています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?