yasudacloudの日記

札幌に住むソフトウェアエンジニア

Leafletでマインドマップを実装する

また少し更新が空いてしまった。。

最近、海外ミステリー/サスペンスに激ハマりしていて、それに関連した投稿になります。

背景

読書というのは定期的にブームが来るもので、「これを読んで純文学が好きになった」とか「この著者の影響を受けてこういう本を読んでみたくなった」という経験は誰もが一度はあるんじゃないでしょうか。

しかし、自分の頭の中にある本の重要度や本同士の関連性、類似性というような感覚的なものを文字やグラフ、数値で適切に表現するのはなかなか難しいです。

ありがちなアプローチとして、強引に数値(点数)をつけて並び替えたり比較することはできますが、色々な角度でいちいち数値化していくのは非常にストレスです。自分の中で明確なランキングを作りたいならこの方法が良いかもしれませんが、今回の件とは趣旨が異なるので割愛します。

僕らITエンジニアはシーケンシャルな"一覧機能"を作ることに長けていますが、今回はあくまで自分の記憶に近い形で情報をアウトプットできるように、という目的です。つまり、データの正規化や管理よりも可視化に重きを置くことが重要になります。

デモ

前置きが長くなったので先に実装したデモ動画をお見せします。

技術選定

まず、この手の描画系のツールは画面を縦横無尽に操作できる必要があります。

前から密かに注目していたLeafletというOSSの地図ライブラリが使えそうなのでこちらを採用します。そういえば以前もLeafletに関する記事を書いた覚えが。

npmのレジストリを探索していると吹き出しとかチャート関連のそれっぽいライブラリが色々ありますが、とりあえず描画関係はこれのみでいきます。安易に導入すると後で実現不可能な要件が出たときに負債になりやすいです。

UIはAnt Designを入れましたがここもあまり重要ではないので省略。

インストール

Reactプロジェクト上でnpm i leaflet react-leafletを実行して準備完了!

実装

長くなるので要所のみ紹介していきます。

まずはReact Leaflet公式のサンプルコードを見てみます。

gist.github.com

MapContainerでは初期表示時の位置(緯度経度)、ズーム、表示範囲などを指定します。今回重要なのは肝心の描画部分であるTileLayerです。

上記のサンプルコードを見ると察すると思いますが、url=の部分を単純に自分でホスティングしているドメインに置き換えれば自前のマップを表示する事が出来ます。ただ、デモ動画を見てわかると思いますが背景は白になっていますし今回の要件でわざわざ画像を使って描画する必要はありません。

そこで、L.TileLayerを継承した独自のクラスを作りフロントエンド側で背景を作っていきます。

最小限のコードはこんな感じでいけます。これだと背景真っ白ですが。

gist.github.com

次にMarker(吹き出し部分)を作っていきます。

gist.github.com

吹き出しはdivIconクラスの中に実装したのですが名前的にここじゃない感があります。Markerの中で他にHTMLを表示する手段が分からなかったので・・。

少しポイントなのが、L.markerに渡している引数が緯度経度ということです。整数でX、Y座標みたいな扱いだったら簡単でしたがベースが地図アプリなので受け入れましょう。

次に吹き出し間の線を作ります。

gist.github.com

線は意外と難しくありませんでした。線で繋ぎたい吹き出し2つの緯度経度を渡すだけです。ただ、このやり方だと直線しか出せず。マインドマップやER図、UMLなんかでは曲線で表現されることも多いので少し気になるところです。

以上が基本的なマインドマップ実装の流れでした。ですが、デモ動画を見てわかる通り他にも色々機能があるので簡単にしていきます。前述のコード以降に処理を追加していく感じです。

吹き出しの移動

markerオブジェクトを生成する際にdraggable: trueを指定します。

線の移動

吹き出しの移動に追随して線も動かしています。これは簡単に変わってくれず、吹き出しが移動するイベント(drag)を拾って移動した方の吹き出しの位置情報を上書きします。

marker.on('drag', (event) => { })

こんな感じで吹き出しのイベントを貼って、↑の中で↓みたいな感じの処理を書いていきます。

gist.github.com

吹き出しのテキストを変更する

前述のコードで作ったdivIconをmarker.setIconメソッドで丸ごと上書きします。

marker.setIcon(divIcon);

上書きというと少しゾッと感じた人もいるかもしれません。イベントリスナー系の処理はメモリリークしても気づきにくいのでログをマメに見ることが大事です。

あとは、MarkerやPolylineなどの生成したオブジェクトをユーザー起点で参照したい場合はmap.eachLayerで一覧取得できるので条件で絞って抽出すると便利です。

gist.github.com

まとめ

少し強引ではありますが、マインドマップ的な表現でメモアプリを作っていけそうな気がしてきました。

マインドマップのSaaSはtoB向けや有償のものが多いので、もう少しソーシャル寄りのサービスとして提供できたら面白いかなーと思っています。

他に欲しい機能として、吹き出しクリックして他の吹き出し(あるいは別のプロジェクトの吹き出し)に遷移するとか、属性の表示を簡単に切り替えれると使い勝手が向上しそうです。

自分の場合は著者や著者の出身地、ジャンル、影響を受けた本の関連などを記録していきたいので視覚的に関係性がパッと把握できるのが理想です。ChatGPTとかに投げると良い感じに整理してくれる気がしていて、インタラクティブに脳内の情報と近い形で描画できると近未来的ですね。

最後に

今年の後半は色々な不運が続いて、本当に心労が大きい一年でした。

目標や学習に対しての振り返りや反省は重要だと思っていますが、言い訳してもしょうがないので、来年もっと良い結果が出せるように生活スタイルや環境から見直したいと思います。あ、前も言ったような🤔

とりあえず年末年始は引きこもってこれ読みます。バリー ライガのミステリー3部作。米ドラマ/映画でもありがちなベタベタ殺人劇とヒューマンドラマ。それがなかなか癖になります。

おわりd( ̄  ̄)