Mapbox Newsletter WEEKLY TIPSの解説 -「ドラッグ可能なポイントを作成」
はじめに
この記事は、先日配信されたMapbox NewsletterのWEEKLY TIPSで紹介されていた「ドラッグ可能なポイントを作成」についての解説です。また、Newsletterの購読はこちらからお申し込みいただけます。
コードを確認
まずExamplesのコードを見に行きましょう。
日本語サイト
英語サイト
基本的に同じコードですが、英語版はスタイルがMapbox Streets v12にアップグレードされているのでこちらを使用します。Mapbox Streets v11ではデフォルトのプロジェクションがWebメルカトルであるのに対し、Mapbox Streets v12ではGlobe(3D表示された地球)なので、印象がかなり異なります。
HTML/CSS
まずHTMLを見ていきましょう。
以下は地図を表示するエレメントです。
<div id="map"></div>
以下はポイントをドラッグ&ドロップした際に左下に表示される軽度・緯度を表示するエレメントの定義です。
<pre id="coordinates" class="coordinates"></pre>
また、このエレメントは以下のスタイルが使用されています。
.coordinates {
background: rgba(0, 0, 0, 0.5);
color: #fff;
position: absolute;
bottom: 40px;
left: 10px;
padding: 5px 10px;
margin: 0;
font-size: 11px;
line-height: 18px;
border-radius: 3px;
display: none;
}
Mapの作成
次にJavaScriptのコードを見ていきます。以下のコードはいつも通り、Mapオブジェクトを作成しています。container
で地図を表示するHTMLエレメントのidを指定します。
const map = new mapboxgl.Map({
container: 'map',
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/streets-v12',
center: [0, 0],
zoom: 2
});
レイヤーの追加
地図上に表示されている赤いポイント(丸)はサークルレイヤーとして実装されています。そこで、load
イベント(map.on('load', () => {})
)の中でレイヤーを作成しています。
以下ではまずソースを登録しています。ここではGeoJSONをソースとしています。
// Add a single point to the map.
map.addSource('point', {
'type': 'geojson',
'data': geojson
});
geojson
という変数は以下の場所で定義されています。ポイントデータで、座標が[0 ,0]
なので、初期状態ではポイントが東経0°、北緯0°に表示されています。
const geojson = {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [0, 0]
}
}
]
};
次にこのソースを使用してレイヤーを作成します。circle-radius
でポイントのサイズ(直径)、circle-color
でポイントの色が変わるので試してみてください。
map.addLayer({
'id': 'point',
'type': 'circle',
'source': 'point',
'paint': {
'circle-radius': 10,
'circle-color': '#F84C4C' // red color
}
});
他にも設定可能なプロパティがいくつもあります。詳細は以下のサイトをご参照ください。
地図上でのマウスイベント処理
Map#on
メソッドはon(type, layerId, listener)
という定義を持ちます。このメソッドはtype
で指定されたイベントがlayerId
で指定されたレイヤーのFeature上で発生したときにlistner
の処理が行われます。
サンプルコードではいくつかのマウスイベントが使用されています。一つずつ見ていきましょう。
mouseenter
map.on('mouseenter', 'point', () => {
map.setPaintProperty('point', 'circle-color', '#3bb2d0');
canvas.style.cursor = 'move';
});
ポイントが描画されている領域にマウスカーソルが入ったときに行われる処理を定義しています。具体的にはポイントの色とマウスカーソルを変化させています。
mouseleave
map.on('mouseleave', 'point', () => {
map.setPaintProperty('point', 'circle-color', '#3887be');
canvas.style.cursor = '';
});
ポイントが描画されている領域からマウスカーソルが出たときに行われる処理を定義しています。具体的にはポイントの色とマウスカーソルを変化させています。
mousedown
map.on('mousedown', 'point', (e) => {
// Prevent the default map drag behavior.
e.preventDefault();
canvas.style.cursor = 'grab';
map.on('mousemove', onMove);
map.once('mouseup', onUp);
});
ポイント上でマウスマウスのクリックボタンを押下したときに行われる処理を定義しています。通常マウスのドラッグ操作は地図全体をパン(スクロール)させますが、ここではポイントだけを移動させたいのでpreventDefault
でデフォルトのマウス操作に関する挙動を止めています。さらにマウスカーソルを変化させてから2つのイベントを追加しています。
Map#on
にはon(type, listener)
というもう一つの定義があります。こちらは特定のレイヤーではなく地図全体に対するイベントを処理します。ここではmousemove
(マウスカーソルの移動)が発生した際に、以下に定義されるonMove
の処理を行います。この処理は、マウスカーソルの現在の地図上の座標を取得し、ソースのデータを更新しています。ソースのデータがsetData
で更新されると、ポイントが新しいソースの座標(つまりマウスカーソルの位置)に再描画されます。
function onMove(e) {
const coords = e.lngLat;
// Set a UI indicator for dragging.
canvas.style.cursor = 'grabbing';
// Update the Point feature in `geojson` coordinates
// and call setData to the source layer `point` on it.
geojson.features[0].geometry.coordinates = [coords.lng, coords.lat];
map.getSource('point').setData(geojson);
}
ちなみに、map.on('mousemove', 'point', onMove)
を使ってもある程度動きますが、マウスカーソルを大きく動かした場合にポイントが追従しません。これは、マウスカーソルがポイントの外に出てしまい、onMove
が呼ばれなくなるためです。そのため、ここではmap.on('mousemove', onMove)
を使用しています。
Map#once
は次に該当イベントが発生した際に一度だけ実行されます。ここではmouseup
(マウスのクリックボタンが離された)が発生した際に、以下に定義されるonUp
の処理を行います。この処理は、マウスカーソルの現在の地図上の座標を取得し、左下のcoordinates
領域に緯度・軽度を表示します。さらに、Map#off
メソッドを使用してmousemove
およびtouchmove
イベントに割り当てられたonMove
メソッドの処理を削除します。
function onUp(e) {
const coords = e.lngLat;
// Print the coordinates of where the point had
// finished being dragged to on the map.
coordinates.style.display = 'block';
coordinates.innerHTML = `Longitude: ${coords.lng}<br />Latitude: ${coords.lat}`;
canvas.style.cursor = '';
// Unbind mouse/touch events
map.off('mousemove', onMove);
map.off('touchmove', onMove);
}
touchstart
map.on('touchstart', 'point', (e) => {
if (e.points.length !== 1) return;
// Prevent the default map drag behavior.
e.preventDefault();
map.on('touchmove', onMove);
map.once('touchend', onUp);
});
touchstart
はスマホなどでタッチ操作した際に発生するイベントです。処理内容は基本的にmousedown
と同じです。
まとめ
「ドラッグ可能なポイントを作成」はサークルレイヤーとしてポイントを実装し、マウスイベントを自分で記述することでドラッグ処理を実現していました。少し複雑に見えますが、ひとつひとつ見ていくと理解できるかと思います。
おまけ
mousedown
イベントでpreventDefault
を用いて「デフォルトのマウス操作に関する挙動を止めて」いました。ということは、mouseup
のときに「デフォルトのマウス操作に関する挙動を動かす」必要があるのでは?という疑問が湧きます。実際にはその様なコードはありませんが、mouseup
後に正しく地図をパン(スクロール)できるので、問題なく動いているようです。
すこしSDKのコードを覗いてみましょう。まず、地図をパンするために、内部的には以下の処理が行われます。
さて、コードの中で書いたmousedown
のコールバック関数はMapEventHandler
として処理されます。MapEventHandler
はMouseHandler
(パンの処理をするクラス)のよりも先行して実行されます。そこで、コールバック関数の中でe.preventDefault()
を実行すると後続のMouseHandler
の処理がブロックされます。これにより1.の初期座標が記録されません。
その後、ドラッグを行うとmousemove
イベントはMouseHandler
にも通知されますが、初期座標がないためパンの処理は行われません。
つまり、preventDefault
は「デフォルトのマウス操作に関する挙動を止める」というよりは、「そのタイミングのmousedown
時の処理を行わせない」と表現するほうが正確です。また、これにより初期座標が記録されていないため、付随的にパンの処理も行われません。しかし、次回のポイント以外の場所でのmousedown
では初期座標が記録されるので付随するパンの処理も実行されます。
Discussion