🌍

Mapbox Newsletter WEEKLY TIPSの解説 -「クラスターの作成と設定」

2024/08/06に公開

はじめに

この記事は、先日配信されたMapbox NewsletterのWEEKLY TIPSで紹介されていた「クラスターの作成と設定」についての解説です。このサンプルではクラスターの使い方を例示しています。また、Newsletterの購読はこちらからお申し込みいただけます。

以下が本サンプルのデモです。

コードを確認

まずExamplesのコードを見に行きましょう。

日本語サイト

英語サイト

基本的に同じコードですが、英語版はスタイルがMapbox Dark v11にアップグレードされているのでこちらを使用します。Mapbox Dark v10ではデフォルトのプロジェクション(地図投影法)がWebメルカトルであるのに対し、Mapbox Dark v11ではGlobe(3D表示された地球)なので、印象がかなり異なります。また、英語版はMapbox GL JS v3が使用されています。

HTML/CSS

まずHTMLを見ていきましょう。

以下は地図を表示するエレメントです。

<div id="map"></div>

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/dark-v11',
  center: [-103.5917, 40.6699],
  zoom: 3
});

loadイベント

addSourceおよびaddLayerはスタイルの読み込み後に実行される必要があります。そこで、loadイベント(map.on('load', () => {})の中身)で以降の処理を記述しています。

ソースの作成

https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson から地震に関するGeoJSONデータを取得し、ソースを作成します。このデータはPointデータです。クラスターを作成するためにはソースにクラスターに関する設定が必要です。これは、クラスターデータを動的に生成しているというソース内部動作に起因します。設定項目はこちらにあります。

map.addSource('earthquakes', {
  type: 'geojson',
  // Point to GeoJSON data. This example visualizes all M1.0+ earthquakes
  // from 12/22/15 to 1/21/16 as logged by USGS' Earthquake hazards program.
  data: 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson',
  cluster: true,
  clusterMaxZoom: 14, // Max zoom to cluster points on
  clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)
});

サンプルの設定項目は以下のとおりです。

項目 説明
cluster クラスターを使用する際にtrueを設定
clusterMaxZoom 設定したズームレベルまでクラスターデータを生成
clusterRadius 指定したピクセル半径内のデータをクラスタリング

レイヤーの作成

3種類のレイヤーを作成します。それぞれ見ていきます。

地震発生件数を示す円

1つ目は地震発生件数を示す円です。Circleレイヤーで作成します。clustertrueにしたソースではclusterRadius半径内に存在するPointデータを結合した新たなPointoデータが自動的に作成されています。このデータはpoint_countというプロパティを持っているので、それのあるなしでクラスターにより作成された新しいデータか元の地震のPointデータかを区別できます (filter: ['has', 'point_count']の部分)。

map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'earthquakes',
  filter: ['has', 'point_count'],
  paint: {
    ...後述...
  }
});

次にpaintの中身を見ます。point_countにはクラスタリングされたPointの個数が入っていますが、この個数に合わせて円の色と大きさを変更します。円の色はcircle-color、円の大きさはcircle-radiusで指定します。それぞれ、stepを使い、閾値ごとに色・大きさを指定しています。

'circle-color': [
  'step',
  ['get', 'point_count'],
  '#51bbd6',
  100,
  '#f1f075',
  750,
  '#f28cb1'
],
'circle-radius': [
  'step',
  ['get', 'point_count'],
  20,
  100,
  30,
  750,
  40
]

stepは以下のような書式です。例えば入力値が閾値1未満の場合は出力値0、入力値が閾値2未満の場合は出力値1が出力されます。今回は入力値としてpoint_countを使用するのでgetを用いて値を取得しています。

["step",
  入力値,
  出力値0,
  閾値1,
  出力値1,
  閾値2,
  出力値2,
  ...
]

stepはその名の通り、閾値前後で出力値が切り替わります。代わりにinterpolateを使うと徐々に色が変わるのでこれも面白いです。

地震発生件数の表示

2つ目は地震発生件数の表示です。数値を表示するのでSymbolレイヤーを使用します。1つ目と同様のfilterを用いてクラスターデータを取得します。また、表示する数値は['get', 'point_count_abbreviated']で取得します。point_count_abbreviatedpoint_countと同じデータですが、概数として格納されています例えば、1234であれば1.2kのような値になっています。

map.addLayer({
  id: 'cluster-count',
  type: 'symbol',
  source: 'earthquakes',
  filter: ['has', 'point_count'],
  layout: {
    'text-field': ['get', 'point_count_abbreviated'],
    'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
    'text-size': 12
  }
});

元の地震データの表示

ズームをしていくと、クラスターから外れて元の地震データとして取得できる点が現れてきます。これを表示するためのCircleレイヤーが3つ目のレイヤーです。クラスターデータではないのでfilterの条件が逆転しています(filter: ['!', ['has', 'point_count']],)。

map.addLayer({
  id: 'unclustered-point',
  type: 'circle',
  source: 'earthquakes',
  filter: ['!', ['has', 'point_count']],
  paint: {
    'circle-color': '#11b4da',
    'circle-radius': 4,
    'circle-stroke-width': 1,
    'circle-stroke-color': '#fff'
  }
});

クリックの処理

クラスター、元の地震データそれぞれのクリックに対する処理が記述されています。

クラスターデータのクリック処理

Map#onclickイベントは第2引数にレイヤーIDを指定できます。ここでは1つ目のレイヤーのID、clustersを指定しています。これにより、clustersレイヤーのフィーチャーがクリックされたときにだけこのイベントが発火します。

map.on('click', 'clusters', (e) => {
  ...後述...
});

イベントが発火するとまずMap#queryRenderedFeatures を実行してクリックした地点にあるclustersレイヤーのフィーチャーを取得します。

const features = map.queryRenderedFeatures(e.point, {
  layers: ['clusters']
});

次にそのクラスタのフィーチャーのcluster_idを取得し、Map#getClusterExpansionZoomを呼び出します。getClusterExpansionZoomは指定したcluster_idのフィーチャーが展開されるズームレベルをコールバック関数に返します。そこで、コールバック関数の中でそのズームレベルまでカメラをズームさせる処理を記述しています。

const clusterId = features[0].properties.cluster_id;
map.getSource('earthquakes').getClusterExpansionZoom(
  clusterId,
  (err, zoom) => {
    if (err) return;

    map.easeTo({
      center: features[0].geometry.coordinates,
      zoom: zoom
    });
  }
);

元データの地震データのクリック処理

同様にクリックイベントの処理です。ここでは3つ目のレイヤーのID、unclustered-pointを指定しています。

map.on('click', 'unclustered-point', (e) => {
  ...後述...
});

クリックしたフィーチャーの座標、マグニチュード、津波の有無を取得します。

const coordinates = e.features[0].geometry.coordinates.slice();
const mag = e.features[0].properties.mag;
const tsunami =
  e.features[0].properties.tsunami === 1 ? 'yes' : 'no';

地図が繰り返されるプロジェクションを使用しているときの処理です(こちらの記事と同じ処理です)。

if (['mercator', 'equirectangular'].includes(map.getProjection().name)) {
  while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
    coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
  }
}

最後に取得した情報を使ってポップアップを表示します。

new mapboxgl.Popup()
  .setLngLat(coordinates)
  .setHTML(
    `magnitude: ${mag}<br>Was there a tsunami?: ${tsunami}`
  )
  .addTo(map);

マウスカーソルの変更

1つ目のレイヤーにマウスカーソルが乗ったときにマウスカーソルを変更します。また、外れたときには元に戻します。

 map.on('mouseenter', 'clusters', () => {
   map.getCanvas().style.cursor = 'pointer';
 });
 map.on('mouseleave', 'clusters', () => {
   map.getCanvas().style.cursor = '';
 });

まとめ

多数のデータがありそのまま表示すると見にくくなるケースで、クラスターを使うと視覚的にわかりやすい地図が作成できることがわかりました。

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

Discussion