Help us understand the problem. What is going on with this article?

Leaflet.jsのマウスホイールZoomを、Googlemap的な無段階ズームにする

モチベーション

Leaflet.jsは使い勝手がシンプルで、D3やPixiJSなどと組み合わせてオリジナルのレイヤーを拡張することが割と自由にできて気に入っています。

ただ、1点、マウスホイールでのズームが段階ズームであることが気に入らなかったので、無段階化するプラグインもどきを作ってみました。
(leaflet.jsはデフォルトでズームのステップを細かくすることはできるのですが、ホイールを回した量に応じてズーム量を変えることができない)

デモはこちら

イメージ
demo_short.gif

できたものはGitHubに上げております。

方法

実は作ったのはずいぶん前なので細かい部分までは覚えていないのですが、leafletはピンチイン/アウトでのズームにも対応しており、こちらは無段階ズームです。
なので、マウスホイールイベントをトリガーとして、ピンチイン/アウトしたときの処理内容を移植したプラグインを作ればよいのではないかと思いました。

githubのleaflertを見て、プラグインの作り方を学ぶ

leafletは本体自体もプラグインを組み合わせて機能を増やしていく構造になっていました。
マウスホイールズーム、ピンチイン/アウトを実現しているプラグインは以下の通りです。

ピンチイン/アウトのプラグイン(構造のみ抜粋)
Map.TouchZoom.js
export var TouchZoom = Handler.extend({
    addHooks: function () {
        DomUtil.addClass(this._map._container, 'leaflet-touch-zoom');
        DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this);
    },

    removeHooks: function () {
        DomUtil.removeClass(this._map._container, 'leaflet-touch-zoom');
        DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this);
    },

    _onTouchStart: function (e) {
        ... 中略
    },

    _onTouchMove: function (e) {
        ... 中略
    },

    _onTouchEnd: function () {
        ... 中略
    }
});
マウスホイールズームのプラグイン(構造のみ抜粋)
Map.ScrollWheelZoom.js
L.Map.SmoothWheelZoom = L.Handler.extend({

    addHooks: function () {
        L.DomEvent.on(this._map._container, 'mousewheel', this._onWheelScroll, this);
    },

    removeHooks: function () {
        L.DomEvent.off(this._map._container, 'mousewheel', this._onWheelScroll, this);
    },

    _onWheelScroll: function (e) {
        _onWheelScroll: function (e) {
        if (!this._isWheeling) {
            this._onWheelStart(e);
        }
        this._onWheeling(e);
    },

    _onWheelStart: function (e) {
        var map = this._map;
        this._isWheeling = true;
        ...中略
        this._zoomAnimationId = requestAnimationFrame(this._updateWheelZoom.bind(this));
    },

    _onWheeling: function (e) {
        ...中略
        clearTimeout(this._timeoutId);
        this._timeoutId = setTimeout(this._onWheelEnd.bind(this), 200);
    },

    _onWheelEnd: function (e) {
        this._isWheeling = false;
        cancelAnimationFrame(this._zoomAnimationId);
    },

    _updateWheelZoom: function () {
        ...中略
    }

});

構造をみてみると、マウスホイールは開始、終了イベントがないため、
1、フラグを使って開始イベントを疑似的発生させて目的のズーム率を設定
2、requestAnimationFrameで目的のズーム率に向かってアニメーション
3、一定時間触っていないことをsetTimeoutで判定して疑似的に終了イベントを発生させてズーム終了
みたいになっています。
多少違いはあるものの、開始、ズーム中、終了に分かれてるので、単純に移植できそうです。

完成版

SmoothWheelZoom.js
L.Map.SmoothWheelZoom = L.Handler.extend({

    addHooks: function () {
        L.DomEvent.on(this._map._container, 'mousewheel', this._onWheelScroll, this);
    },

    removeHooks: function () {
        L.DomEvent.off(this._map._container, 'mousewheel', this._onWheelScroll, this);
    },

    _onWheelScroll: function (e) {
        if (!this._isWheeling) {
            this._onWheelStart(e);
        }
        this._onWheeling(e);
    },

    _onWheelStart: function (e) {
        var map = this._map;
        this._isWheeling = true;
        this._wheelMousePosition = map.mouseEventToContainerPoint(e);
        this._centerPoint = map.getSize()._divideBy(2);
        this._startLatLng = map.containerPointToLatLng(this._centerPoint);
        this._wheelStartLatLng = map.containerPointToLatLng(this._wheelMousePosition);
        this._startZoom = map.getZoom();
        this._moved = false;
        this._zooming = true;

        map._stop();
        if (map._panAnim) map._panAnim.stop();

        this._goalZoom = map.getZoom();
        this._prevCenter = map.getCenter();
        this._prevZoom = map.getZoom();

        this._zoomAnimationId = requestAnimationFrame(this._updateWheelZoom.bind(this));
    },

    _onWheeling: function (e) {
        var map = this._map;

        this._goalZoom = this._goalZoom - e.deltaY * 0.003 * map.options.smoothSensitivity;
        if (this._goalZoom < map.getMinZoom() || this._goalZoom > map.getMaxZoom()) {
            this._goalZoom = map._limitZoom(this._goalZoom);
        }
        this._wheelMousePosition = this._map.mouseEventToContainerPoint(e);

        clearTimeout(this._timeoutId);
        this._timeoutId = setTimeout(this._onWheelEnd.bind(this), 200);
    },

    _onWheelEnd: function (e) {
        this._isWheeling = false;
        cancelAnimationFrame(this._zoomAnimationId);
    },

    _updateWheelZoom: function () {
        var map = this._map;

        if ((!map.getCenter().equals(this._prevCenter)) || map.getZoom() != this._prevZoom)
            return;

        this._zoom = map.getZoom() + (this._goalZoom - map.getZoom()) * 0.3;
        this._zoom = Math.floor(this._zoom * 100) / 100;

        var delta = this._wheelMousePosition.subtract(this._centerPoint);
        if (delta.x === 0 && delta.y === 0)
            return;

        if (map.options.smoothWheelZoom === 'center') {
            this._center = this._startLatLng;
        } else {
            this._center = map.unproject(map.project(this._wheelStartLatLng, this._zoom).subtract(delta), this._zoom);
        }

        if (!this._moved) {
            map._moveStart(true, false);
            this._moved = true;
        }

        map._move(this._center, this._zoom);
        this._prevCenter = map.getCenter();
        this._prevZoom = map.getZoom();

        this._zoomAnimationId = requestAnimationFrame(this._updateWheelZoom.bind(this));
    }

});

詳しい解説はしませんが(もう忘れてる)、Map.ScrollWheelZoom.jsの構造にMap.TouchZoom.jsの処理を入れこむような形です。

使い方

デフォルトのScrollWheelZoomをオフにして、作ったプラグインをオンにします。

var map = L.map('map', {
  scrollWheelZoom: false, // disable original zoom function
  smoothWheelZoom: true,  // enable smooth zoom 
  smoothSensitivity: 1,   // zoom speed. default is 1
});

使って頂けるのもうれしいですし、自分でleafletをカスタマイズする際の参考になれば幸いです。

デモ

本当はyarnとかでインストールできるようにとかしてみたいんですが、なかなか手をだせてません。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away