• #leaflet
  • #地図
  • #データ可視化
  • #tokyo-soundscape
  • #javascript
  • #excel
tokyo-onkeiメモ

Leaflet.jsで東京再開発フィールドワークマップを構築した

東京の再開発エリアと音のフィールドワークを計画するためのインタラクティブ地図を、Leaflet.jsで構築した。Excelに整理していたデータ(再開発タイムライン29件、ルート計画9クラスタ、木密地域12箇所)を地図上に可視化して、収録計画のハブとして使えるようにするのが目的。

データ構成

入力データはExcelで管理していた3種類のシート。

  • 再開発タイムライン: 29件の再開発プロジェクト(竣工予定日付、優先度付き)
  • ルート計画: 9クラスタに分けた収録ルート(月別アクション付き)
  • 木密地域: 12箇所の木造密集地域(再開発前の音を記録するための候補地)

これらをJSONに変換し、Leaflet上でマーカーとカードとして表示する構成にした。

ポップアップからカード常時表示へ

最初はLeafletの標準ポップアップを使っていた。マーカーをクリックすると情報が出るシンプルな方式。しかし、地図を一覧として眺めたいユースケースではクリックが手間になる。

カードを地図の周囲に常時表示する方式に変更した。地図の上下左右にカードを配置して、各カードからマーカーへ線を引く形。

4辺配置から左右優先配置へ

最初は上下左右の4辺にカードを均等配置していたが、上下のカードは地図の表示領域を圧迫するし、線も長くなりがちだった。左右の列に優先的にカードを配置する方式に変更した結果、地図の中央エリアが広く使えるようになった。

巨大な青ピン問題

マーカーを表示したところ、Leafletのデフォルトピンが異常に巨大になる現象に遭遇した。原因を調べたところ、ページ内のCSS #overlay svg ルールがLeafletのSVGレイヤーにまで影響していた。

/* これが原因だった */
#overlay svg {
  width: 100%;
  height: 100%;
}

LeafletはSVGでマーカーを描画するため、この汎用ルールがピンのサイズを壊していた。対策としてL.markerを使うのをやめてL.circleMarkerに切り替えた。circleMarkerはSVGの<circle>要素で描画されるため、サイズ指定がradiusで完結し、外部CSSの影響を受けにくい。

カード重なりの解消

カードを左右に並べるとき、カード同士が重なる問題があった。原因は CARD_H(カードの高さ)の計測タイミングと実際のレンダリング後の高さに不整合があったこと。

DOMに追加した直後に getBoundingClientRect() で高さを取ると、画像の読み込みやフォントのレンダリングが完了していないため実際より小さい値になる。固定のカード高さを使うか、全カードのレンダリング完了後にまとめて再配置する方式で解消した。

優先度フィルタ

データの優先度(最優先 / 高 / 中 / 低 / 木密地域)でフィルタリングできるタブUIを実装した。タブを切り替えると該当するマーカーとカードだけが表示される。

フィルタ切り替え時にdetailパネルが前のマーカーの情報を表示したままになるバグがあり、フィルタ変更時にdetailパネルをリセットする処理を追加した。

自動ズーム

フィルタを切り替えたときに、表示中のマーカーが全て見える範囲に自動ズームする機能を入れた。ただし左右にカード列があるため、地図の全幅でfitBoundsするとマーカーがカードの下に隠れる。

カード列の幅を除いた中央エリアのみを対象にマーカーがフィットするよう、fitBoundspaddingを動的に計算する方式にした。

Unsplash画像からWikimedia Commonsへの差し替え

当初はUnsplashのフリー写真をカードのサムネイルに使っていたが、実際の場所の写真のほうが地図としての価値が高い。Wikimedia Commonsから各スポットの実際の写真を探して、全41件を差し替えた。

ライセンスはCC BY-SA等のWikimediaの各画像のライセンスに従う形。

カテゴリ内番号(catNo)

マーカーとカードにカテゴリ内の連番を振った。「最優先-1」「高-3」のように、フィルタカテゴリ内での順番がわかるようにする機能。フィルタで絞り込んだときに「このカテゴリに何件あって、今何番目を見ているか」が直感的にわかる。

detailパネルの拡充

マーカーまたはカードをクリックしたときに開くdetailパネルに、Excel由来の詳細情報を追加した。

  • 竣工予定日付(再開発タイムラインのデータ)
  • ルート計画(どのクラスタに属するか、推奨ルート)
  • 月別アクション(何月に何を収録するかの計画)

テキスト視認性の改善

地図タイルの上にテキストを重ねるため、視認性の確保が必要だった。黒背景のカードやパネルでは白文字にし、opacity値を引き上げて可読性を改善した。半透明にしすぎると地図の色と混ざって読めなくなる。

ナビゲーション機能

カード間の移動を左右矢印ボタンとキーボードショートカット(矢印キー)で操作できるようにした。マーカー間を移動するときにズームインアニメーション(flyTo)を入れて、移動先がどこかわかりやすくした。

左サイドバーにはスクロール可能なカードリストを配置し、一覧からも選択できるようにした。

rebuildTimerのsetTimeoutループ修正

フィルタ切り替えやリサイズ時にカードを再配置する処理で、setTimeoutのループが残る問題があった。clearTimeoutでキャンセルせずに新しいタイマーを積み重ねていたため、再配置が複数回走ってちらつきが出ていた。

// 修正前: タイマーが積み重なる
setTimeout(() => rebuild(), 200);

// 修正後: 前のタイマーをキャンセル
clearTimeout(rebuildTimer);
rebuildTimer = setTimeout(() => rebuild(), 200);

デバウンス処理の基本だが、変数のスコープが関数内に閉じていて外からclearTimeoutできない状態だった。モジュールスコープに変数を移動して解消した。

ふりかえり

Leafletそのものはシンプルなライブラリだが、実際の地図アプリケーションでは周辺のUI(カード配置、フィルタ、パネル、ナビゲーション)のほうが実装量が多い。マーカーを打つだけなら30分で終わるが、使いやすい地図ツールにするには一日がかりになった。

CSSの汎用ルールがLeafletのSVG描画に影響する問題は、ライブラリを組み合わせる際の典型的な落とし穴だと思う。#overlay svgのような広いセレクタは避けるか、Leafletのコンテナにスコープを切るべきだった。

ExcelからJSONへの変換は手動で行ったが、データ量が増えるならスクリプト化したほうがいい。今回は41件だったので手作業でもなんとかなった。

次のステップ

  • モバイル対応(カード列をドロワーに切り替え)
  • 収録済みマーカーのステータス管理(未収録/収録済み/編集済み)
  • 音声プレビュー機能(マーカークリックでサンプル音源を再生)