🙆

Mapbox Newsletter WEEKLY TIPSの解説 -「ドラッグ可能なポイントを作成」

2023/05/19に公開

はじめに

この記事は、先日配信された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のコードを覗いてみましょう。まず、地図をパンするために、内部的には以下の処理が行われます。

  1. mousedown時の座標を初期座標として記録(コード
  2. マウスカーソルが移動するたびに初期座標との差分を算出し、パンを実行(コード

さて、コードの中で書いたmousedownのコールバック関数はMapEventHandlerとして処理されます。MapEventHandlerMouseHandler(パンの処理をするクラス)のよりも先行して実行されます。そこで、コールバック関数の中でe.preventDefault()を実行すると後続のMouseHandlerの処理がブロックされます。これにより1.の初期座標が記録されません。

その後、ドラッグを行うとmousemoveイベントはMouseHandlerにも通知されますが、初期座標がないためパンの処理は行われません。

つまり、preventDefaultは「デフォルトのマウス操作に関する挙動を止める」というよりは、「そのタイミングのmousedown時の処理を行わせない」と表現するほうが正確です。また、これにより初期座標が記録されていないため、付随的にパンの処理も行われません。しかし、次回のポイント以外の場所でのmousedownでは初期座標が記録されるので付随するパンの処理も実行されます。

GitHubで編集を提案
マップボックス・ジャパン合同会社

Discussion