Mapbox Newsletter WEEKLY TIPSの解説 -「マップ間のスワイプ」
はじめに
この記事は、先日配信されたMapbox NewsletterのWEEKLY TIPSで紹介されていた「マップ間のスワイプ」についての解説です。このサンプルはmapbox-gl-compareプラグインの使い方に関して例示しています。また、Newsletterの購読はこちらからお申し込みいただけます。
以下が本サンプルのデモです。
コードを確認
まずExamplesのコードを見に行きましょう。
日本語サイト
英語サイト
基本的に同じコードですが、英語版はスタイルがMapbox Light/Dark v11にアップグレードされているのでこちらを使用します。Mapbox Light/Dark v10ではデフォルトのプロジェクションがWebメルカトルであるのに対し、Mapbox Light v11ではGlobe(3D表示された地球)なので、印象がかなり異なります。また、英語版はMapbox GL JS v3が使用されています。
HTML/CSS
まずHTMLを見ていきましょう。
mapbox-gl-compareプラグインの読み込みを行います。
<script src="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-compare/v0.4.0/mapbox-gl-compare.js"></script>
<link rel="stylesheet" href="https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-compare/v0.4.0/mapbox-gl-compare.css" type="text/css">
次に地図を表示するエレメントを作成しています。2つの地図を比較するということで、地図を表示するdivを2つ、そしてそれらをまとめるdivを作成します。
<div id="comparison-container">
<div id="before" class="map"></div>
<div id="after" class="map"></div>
</div>
Mapの作成
次にJavaScriptのコードを見ていきます。以下のコードはいつも通り、Mapオブジェクトを作成しています。containerで地図を表示するHTMLエレメントのidを指定します。ただし、今回は2個作成しています。
const beforeMap = new mapboxgl.Map({
container: 'before',
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/light-v11',
center: [0, 0],
zoom: 0
});
const afterMap = new mapboxgl.Map({
container: 'after',
style: 'mapbox://styles/mapbox/dark-v11',
center: [0, 0],
zoom: 0
});
mapbox-gl-compareの作成
Compareをインスタンス化します。第1、第2引数が作成したMapオブジェクト、第3引数が2つの地図のコンテナのセレクタ、第4引数はオプションです。第3引数はセレクタということに注意が必要です。今回はdivエレメントのidを指定しているので先頭に#がついています。このコンテナの直下にmapbox-gl-compareの本体が配置されます。
const container = '#comparison-container';
const map = new mapboxgl.Compare(beforeMap, afterMap, container, {
// Set this to enable comparing two maps by mouse movement:
// mousemove: true
});
ちなみに、コメントを外してmousemoveオプションを有効化するとドラッグではなくマウスの移動で境目が動きます。
まとめ
非常に簡単に2つの地図を比較することができることがわかりました。また、せっかくなのでMapbox StandardとMapbox Streets v12を比較するものも作ってみました。東京タワー周辺の地図です。ぜひ2つのスタイルを比較してみてください。
おまけ
mapbox-gl-compareはどうやって2つの地図を合成しているのでしょうか。
compareの作成処理
Compareのインスタンス化の方法を見ると、あたかもmapbox-gl-compareが2つの地図を内部に抱え込んでいるように思えます。しかし、実際にコードを見てみるとmapboxgl-compareというクラス名のdivエレメントをコンテナの子要素として追加しているだけです。
このクラスのスタイルは以下のとおりですが、これが真ん中に表示される仕切りの部分です。
そしてmapboxgl-compareにcompare-swiper-verticalというクラス名のdivエレメントが追加されます。
このクラスのスタイルは以下のとおりですが、これが真ん中に表示される丸いマウスでドラッグするパーツです。
つまり、以下のような構造になります。地図を内包しているどころか、2つの地図の下に棒と丸が配置されているだけです。
<div id="comparison-container">
<div id="before" class="map"></div>
<div id="after" class="map"></div>
<!-- ここ -->
<div class="mapboxgl-compare">
<div class="compare-swiper-vertical"></div>
</div>
</div>
css
cssの定義を見て実際の表示を確認します。
2つの地図については以下の通り、abusoluteが指定されているのでピッタリ重なっています。
.map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
mapboxgl-compareとcompare-swiper-verticalもabusoluteなので、地図2枚の上に棒と丸が重なっているという配置になります。
ドラッグ
compare-swiper-verticalをドラッグすると以下の処理が実行されます。
ここではマウスの座標に合わせてclipを使って切り出しています。1つ目のMapはマウスより左側の矩形、2つ目のMapはマウスより右側の矩形が表示されることになります。
var clipA = this._horizontal
? 'rect(0, 999em, ' + x + 'px, 0)'
: 'rect(0, ' + x + 'px, ' + this._bounds.height + 'px, 0)';
var clipB = this._horizontal
? 'rect(' + x + 'px, 999em, ' + this._bounds.height + 'px,0)'
: 'rect(0, 999em, ' + this._bounds.height + 'px,' + x + 'px)';
this._mapA.getContainer().style.clip = clipA;
this._mapB.getContainer().style.clip = clipB;
つまり、ドラッグに合わせてclipが連動するので2つの地図の見える範囲が変わるということになります。
2つの地図が同期して動く仕組み
mapbox-gl-sync-moveというライブラリを使用して同期しています。
まず、以下の部分でmapbox-gl-sync-moveのsyncMaps関数を呼び出しています。
syncMaps関数は何個でもMapオブジェクトを受け取れます。
少しややこしいですが各Mapオブジェクトに対してsync関数を呼び出す処理を割り当てています。この割り当てられた処理はMapのmoveイベントが発火した際に実行されます。また、引数として自分と自分以外のMapを渡しています。
sync関数はマウスイベントを受け取ったMapオブジェクトをmaster、それ以外のMapオブジェクトをcloneとしてmoveToMapPosition関数を実行します。
moveToMapPosition関数は以下の通り、cloneをmasterのカメラに合わせるような処理となります。
すべてのMapについて、操作されたMapがmaster、それ以外がcloneとなっていたので、どのマップを操作してもそれ以外のマップが同じ動きをするようになります。
以下はmapbox-gl-sync-moveのデモです。すべてのマップが同期していますね。
Discussion