いきなり日曜大工

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

Leaflet 拡張基礎編 #2 L.Handler でイベントハンドリング

L.Handler のサブクラスのサンプル

L.Handler の継承について詳しいやり方を見て、それから使い方にいきます。

今回から JavaScript を書くことになりますが、現在の学習進捗は ES5 + α ぐらいです。いずれ ES6 も覚えて使いたいと思っていますが、当面は大体 ES5 + const, let でいきます。

雛型

公式サイトの L.Handler のチュートリアル にのっている extend() を使った L.Handler 継承の雛型です。

L.CustomHandler = L.Handler.extend({
    addHooks: function() {
        L.DomEvent.on(document, 'eventname', this._doSomething, this);
    },

    removeHooks: function() {
        L.DomEvent.off(document, 'eventname', this._doSomething, this);
    },

    _doSomething: function(event) { … }
});

この雛型から実際にサブクラスを作る段取りをまとめます。

  1. extend() を使って L.Handler を継承する
  2. 必要に応じて L.Handler のメソッドを実装する
  3. 必要に応じてメソッド・プロパティを追加する

L.Handler のメソッド実装

上の雛型を元に、マップを動かしたら URL を地図中心の緯度経度とズームレベルに応じて Google Maps 風に書き換える L.Handler のシンプルなサブクラスを作ってみました(実用には遠いものです)。

L.CustomHandler = L.Handler.extend({
    initialize: function(map) {
        L.Handler.prototype.initialize.call(this, map);
        this._pathname = location.pathname.replace(/\/@.*$/, '');
    },

    addHooks: function() {
        this._map.on('moveend', this._onMoveEnd, this);
    },

    removeHooks: function() {
        this._map.off('moveend', this._onMoveEnd, this);
    },

    _onMoveEnd: function(event) {
        const coord = this._map.getCenter().wrap(),
              zoom = this._map.getZoom();

        const lat = L.Util.formatNum(coord.lat, 7),
              lng = L.Util.formatNum(coord.lng, 7),
              z = L.Util.formatNum(zoom, 3) + 'z';

        const newUrl = this._pathname + '/@' + [lat, lng, z].join(',');
        history.replaceState(null, null, newUrl);
    }
});
L.customHandler = function(map) {
    return new L.CustomHandler(map);
};

history API を使っていますが次回の話題にする予定です。

initialize(2行目)

コンストラクタ initialize を実装しています。
前回読んだ拡張ガイドでは、コンストラクタの拡張は addInitHook を使うとありましたが、その場合は extend() とは別に

L.CustomHandler = L.Handler.extend({
    ...
});
L.CustomHandler.addInitHook(function() {
  // コンストラクタに追加したいコード
});

と書く必要があります。これを extend() の中にまとめて書いちゃいたかったので、内部で initialize を定義することにしました。

ところで、コードを追加するにもドキュメントに L.Handler の initialize の項目、あるいは特にその部分についての説明がないのでそもそもの挙動がわからない気がする。私が見つけられないだけ?

というわけで直接 Leaflet ソースコードの Handler の initialize 定義部分を見てみると

    initialize: function (map) {
        this._map = map;
    },

とありました。map を引数にとっているので、これに倣って追加コードの前に親クラスの initialize を呼び出して

    initialize: function(map) {
        L.Handler.prototype.initialize.call(this, map);
        this._pathname = location.pathname.replace(/\/@.*$/, '');
    },

としています。

addHooks, removeHooks(7,11行目)

API リファレンスの L.Handler には、
「L.Handler を継承するクラスはこの2つのメソッドを実装しなければならない」
とあります。

これは作ったサブクラスを後でアプリケーションに組み込んだときに、このクラスが持つハンドラの機能を動的に ON/OFF できるように L.Handler が設計されているためで

  • メソッド enable() で addHooks() が呼ばれる
  • メソッド disable() で removeHook() が呼ばれる

という仕組みになっています。これに従って、 addHook にはイベントハンドリングを有効にするコードを、removeHook にはイベントハンドリングを無効にするコードを書くことになります。つまりそれぞれイベントリスナーの登録・削除をします。

    addHooks: function() {
        this._map.on('moveend', this._onMoveEnd, this);
    },

    removeHooks: function() {
        this._map.off('moveend', this._onMoveEnd, this);
    },

このサンプルでは、マップを動かし終わったときに発火するイベント moveend のリスナーを登録・削除しています。

ファクトリー関数

あとは Leaflet のお作法に従って、ファクトリー関数も定義しておきます。

 L.customHandler = function(map) {
    return new L.CustomHandler(map);
};

L.Handler サブクラスの使い方

それではこのサンプルを実際に組み込んでみます。

Leaflet 入門編 で作った OpenStreetMap の Hello, World にこのサブクラスのコードを加えて何パターンか作ってみました。

シンプルな使い方

インスタンス生成して enable() すると、addHooks() が呼び出されて実装したハンドラの内容が実行されます。

/* L.CustomHandler の定義部分 */
L.CustomHandler = L.Handler.extend({
 ...
});
L.customHandler = function(map) {
    return new L.CustomHandler(map);
};
/* L.CustomHandler の定義部分 */

const nintendo = L.latLng(34.969760, 135.756195);
const map = L.map('map').setView(nintendo, 15);

const customUrl = L.customHandler(map);
customUrl.enable();

// 以下、 L.TileLayer 等は省略

Leaflet Demo – L.Handler #1

アプリケーションの実行中に機能を ON/OFF したい場合はアプリケーション中に別途フラグを用意して(切替ボタンを置くとか)、enable / disable をそれぞれ呼び出せばよさそうです。

L.Map に組み込む

L.Map の addHandler メソッドを使ってこのハンドラを L.Map があらかじめ持っているハンドラのリストに含めてしまい、L.Map のインスタンス生成時のオプションを true にすることで一緒にインスタンス生成して enable() までしてしまう方法もあります。
L.Handler のチュートリアル にこのやり方がのっています。

/* L.CustomHandler の定義部分 */
L.CustomHandler = L.Handler.extend({
 ...
});
L.customHandler = function(map) {
    return new L.CustomHandler(map);
};
/* L.CustomHandler の定義部分 */
L.Map.addInitHook('addHandler', 'customUrl', L.CustomHandler);

const nintendo = L.latLng(34.969760, 135.756195);
const map = L.map('map', {
    customUrl: true
}).setView(nintendo, 15);

Leaflet Demo – L.Handler #2

このとき生成されたインスタンスは map.customUrl でアクセスできます。

自動化

上の13行目で true にしているオプションを L.Map.mergeOptions(L.Class.mergeOptions)を使ってデフォルトを true にすると機能を ON にするところまで自動化できます。例えばこの L.Map への組み込み部分までを含めたクラスの定義を別ファイルにしてそれを読み込むだけで機能 ON にする、なんてこともできます。

L.CustomHandler.js(別ファイル)

/* L.CustomHandler の定義部分 */
L.CustomHandler = L.Handler.extend({
 ...
});
L.customHandler = function(map) {
    return new L.CustomHandler(map);
};
/* L.CustomHandler の定義部分 */
L.Map.addInitHook('addHandler', 'customUrl', L.CustomHandler);
L.Map.mergeOptions({
    customUrl: true
});

handler3.html

<script src="/journals/demo/L.CustomHandler.js"></script>
<script>
const nintendo = L.latLng(34.969760, 135.756195);
const map = L.map('map').setView(nintendo, 15);
</script>

Leaflet Demo – L.Handler #3

次回、History API を使って Google Maps 風 URL のゴールを目指します。