いきなり日曜大工

好きなことを好きなだけやる

Leaflet 拡張基礎編 #3 History API で URL ハンドラを作ろう

Google Maps 風 URL ハンドラ

では前回詳しく見た L.Handler を使って(今回の話題にはなりませんが)、地図を動かすと URL を Google Maps 風に書き換えてくれる URL ハンドラの完成を目指します。

ゴール設定

ハンドラの機能を考えます。

  1. 地図を動かすと、Google Maps 風に URL が書き換えられる
  2. 地図を動かした履歴をブラウザの戻る・進むボタンでたどれる
  3. ブックマークから地図の状態を復元できる

実際の Google Maps の URL には緯度経度とズームレベルの他に、地物などの細かい表示設定をあらわしていると思われる長い呪文(data=…)がついたりしますが、今回は緯度経度とズームレベルのみを対象にします。

段取り

次にそれぞれを実現できそうな方法を考え(調べ)ます。

  1. 地図を動かし終わると L.Map が moveend を fire するので、地図中央の緯度経度とズームレベルを Google Maps 風に整形した文字列(フラグメント)にして URL に付加または書き換える
  2. History API で地図を動かした履歴を管理する(後述)
  3. ページロード時に URL に 1 のフラグメントが含まれていたら解析して地図中央の座標とズームレベル情報を取り出し、 L.Map で setView() する

URL の書き換えや履歴云々は location とかでやるのかなと漠然と思ってましたが、イマドキは History API を使うらしいです。それで前回 1 の URL 書き換えをとりあえず history.replaceState でやってみたんですが、2 も History API で簡単にできそうなことがわかってきました。

History API でらくらく状態管理

ブラウザの履歴と History API

ブラウザ(タブ1枚)はドキュメントの履歴(session history)を持っています。

新規のドキュメント開くと履歴に新しいエントリ(session history entry)が追加され、履歴を移動すると過去のエントリが呼ばれて以前訪れたページが復元されます。この履歴を構成する個々のエントリには URL をはじめとする状態復元に必要なデータが紐づけられています。

かつての window.history オブジェクトは、メソッド(forward, back, go)で履歴を移動する機能だけを提供していましたが、その後(2011年頃~)新たに履歴とそのエントリが持つデータを操作する機能が提供されるようになり、また履歴のエントリが呼ばれる際にはイベント popstate が発火されるようになりました(ブラウザごとの詳しい実装の話は割愛しますが今日でいうモダンブラウザの話です)。

この履歴と履歴を構成するエントリへの操作手段を提供する新しい History インタフェース(イベント popstate を含む場合も)を世間では “HTML5 History API” と呼んでいるようです。

現在の windows.history

  • 移動メソッド forward, back, go
  • 操作メソッド pushState, replaceState
  • 履歴の長さを返すプロパティ length
  • カレントエントリの state を返すプロパティ state

履歴の操作

history.pushState(state, title[, url])

pushState は履歴に新しいエントリを作り、引数に渡すデータをこのエントリに紐づけます。

このとき引数の url を指定していればページのアドレス(アドレスバーの表記やブックマークに使われるアドレス)が書き換えられますが、ページの遷移は起こりません。またここで指定できる url は Same-origin policy の制約を受けます。

引数 state には任意のオブジェクトを渡すことができ、履歴をさかのぼってこのエントリが呼ばれると popstate 発火を受けてイベントオブジェクトからそのまま取り出せる(event.state で受け取れる)ので、あとは自由に、例えばページの状態復元に使うことができます(そこは自分でやるのだ)。

2番目の引数 title は現在使われていないみたいなので自分は null とでもしておきます。

history.replaceState(state, title[, url])

replaceState は、引数の扱いについて pushState と同じですが、新しい履歴のエントリを作ることなく現在のエントリに紐づけられる state, (title,) url の情報を書き換えるのみです。

popstate 発火時

ブラウザの進む・戻るボタンや履歴移動メソッドで履歴を移動すると都度イベント popstate が発火します。このとき呼ばれた履歴のエントリに url が紐づけされていればページのアドレスはその url に書き換えられ、state を渡していればイベントオブジェクトから event.state として受け取ることができます。

// イベント popstate は履歴をたどると発火
window.addEventListener('popstate', function (event) {
    console.log(event.state);     // pushState/replaceState 時に渡した state
});

地図の状態固有 URL と状態の保存・復元

以上の仕組みを今回の URL ハンドラで考えます。

地図を動かし終わったら発火する moveend を受けて

history.pushState({ moveend 時の中心座標とズームレベル }, null, '状態固有 URL');

地図の状態と状態固有の URL をブラウザの履歴に残します(履歴のエントリを作成)。

履歴をたどって popstate が発火したら URL は push したときのものに書き換えられるので、あとはイベントオブジェクト経由で取り出した state に入っている中心の座標とズームレベルを使って地図を再描画すれば、push したときの状態が再現できるということになります。

window.addEventListener('popstate', function (event) {
    // 実際はもう少しだけ複雑です
    const scene = event.state.scene;
    this._map.setView(scene.center, scene.zoom);
});

replaceState にも使い道があります。
ページを最初に表示したときはカレントエントリが state を持っていないので、そのままだと画面の書き換え(地図の移動 + pushState)をした後に履歴を最初まで戻ってきても空の state が返されるだけで地図の初期状態が自動で復元されることはありません。そこで、ページを開いたときに後で取り出すための地図の初期状態を pushState のときと同じ構造のオブジェクトにまとめて

history.replaceState({ ページロード時の中心座標とズームレベル }, null);

と状態を書き換えてデータを紐づけておくと、他の push した履歴のエントリに対するのと同じ popstate 発火時の処理で初期状態の復元ができるようになります。

参考

でけた

仮完成

あとはブックマークの URL のフラグメントを解析して地図の状態を復元する処理などを追加して、細かい挙動を自分好みにいろいろやった結果、期待通りに動くものができました。今後さらなる情報を URL に含めたくなる可能性や他の機能を追加する上で仕様の追加あるいは変更が出てくると思うので仮完成としておきます。

Demo – L.Handler #4

実装第1号

最後に一般的な地図用の L.CRS.* と L.CRS.Simple で座標の扱いを少々変える処理(L.CRS.Simple でなければ 座標を を wrap() するだけです)を追加して、BotW の地図にも装備しました。

Leaflet で BotW 地図を作ろう、の機能第1号「URL ハンドラ」です \\٩(๑`▽´๑)۶//

BotW Demo #1 – Custom URL

ゲームマップだし、URL の座標をピクセルベースの整数にするか検討しましたが、思うところあってひとまず緯度経度のままでいくことにします。

やっと地図に機能をつけるところまできました。まだスクリプト1つ書いただけなのにここまでえらい長かったような。