🛣️

Mapbox Newsletter WEEKLY TIPSの解説 -「距離を測定」

2024/02/09に公開

はじめに

この記事は、先日配信されたMapbox NewsletterのWEEKLY TIPSで紹介されていた「距離を測定」についての解説です。このサンプルはturf.lineDistanceの使い方について例示しています。また、Newsletterの購読はこちらからお申し込みいただけます。

以下が本サンプルのデモです。地図上をクリックしていくと点が打たれ、点と点の間に線分が描画されます。また、点と点の間の距離が表示されます。点をクリックするとその点を消せます。

Turf

Turfは地理情報の分析等を行うJavaScriptのライブラリです。Mapbox GL JSと組み合わせて使うことができます。

今回使用するlengthのドキュメントはこちらです。使い方は以下の通り、引数にLineStringを含むGeoJSONを入れるだけです(turf.lineStringLineStringを含むGeoJSONを作成します)。

var line = turf.lineString([[115, -32], [131, -22], [143, -25], [150, -34]]);
var length = turf.length(line, {units: 'miles'});

コードを確認

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

日本語サイト

英語サイト

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

HTML

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

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

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

以下は距離を表示するエレメントです。

<div id="distance" class="distance-container"></div>

CSS

次にCSSを確認します。先程HTMLで作成したdistanceに対して設定するスタイルが定義されています。

以下はdivそのものに対するスタイルの指定です。

.distance-container {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1;
}

以下はdivの子エレメントに対するスタイルの指定です。

.distance-container > * {
  background-color: rgba(0, 0, 0, 0.5);
  color: #fff;
  font-size: 11px;
  line-height: 18px;
  display: block;
  margin: 0;
  padding: 5px 10px;
  border-radius: 3px;
}

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: [2.3399, 48.8555],
  zoom: 12
});

距離計測の準備

以下は距離を表示するHTMLエレメントを取得しています。

const distanceContainer = document.getElementById('distance');

マウスクリックで点および線分が描画されますが、以下はその際に使用する変数です。GeoJSONおよびGeoJSONの中に入れるLineStringです。

// GeoJSON object to hold our measurement features
const geojson = {
  'type': 'FeatureCollection',
  'features': []
};
 
// Used to draw a line between points
const linestring = {
  'type': 'Feature',
  'geometry': {
    'type': 'LineString',
    'coordinates': []
  }
};

ソース・レイヤーの作成

マウスクリックの際描画される点および線分の描画にはレイヤーを使用します。そこでソースとレイヤーを作成します。ソースとレイヤーは地図のロード後に作成するためmap.on('load', ()=>{/* ここ */})の「ここ」の部分に記載します。

まずはソースの作成です。先程作成した変数geojsonをデータとしてGeoJSONソースを作成します。最初はgeojsonfeaturesが空配列なので、GeoJSONとしては空データになります。

map.addSource('geojson', {
  'type': 'geojson',
  'data': geojson
});

1つ目のレイヤーはクリック時に表示される点を描画するcircleレイヤーです。レイヤーidはmeasure-pointsです。ソースは先程のgeojsonを使用します。filterinはフィーチャー(ここではGeoJSON)の中から条件にマッチするものだけを返すフィルタ表現です。第二引数はフィーチャーのプロパティ名か、$type/$idを指定します。第三引数はマッチする値を指定します。$typeの場合は第三引数にPointLineStringまたはPolygonを指定します。例えば、Pointを指定すると、Pointデータのみがマッチします。ここではcircleレイヤーで点を描画したいのでPointデータのみを取得するためにこのフィルタを設定しています。

// Add styles to the map
map.addLayer({
  id: 'measure-points',
  type: 'circle',
  source: 'geojson',
  paint: {
    'circle-radius': 5,
    'circle-color': '#000'
  },
  filter: ['in', '$type', 'Point']
});

2つ目のレイヤーは点と点の間に描画される線分を表現するlineレイヤーです。レイヤーidはmeasure-linesです。ソースは先程のgeojsonを使用します。curcleレイヤーと同様にLineStringデータのみをフィルタして表示します。

map.addLayer({
  id: 'measure-lines',
  type: 'line',
  source: 'geojson',
  layout: {
    'line-cap': 'round',
    'line-join': 'round'
  },
  paint: {
    'line-color': '#000',
    'line-width': 2.5
  },
  filter: ['in', '$type', 'LineString']
});

クリック時の動作

いよいよメインのクリック時の動作です。地図上のクリックイベントに対する処理はmap.on('click', (e) => {/* ここ */})の「ここ」の部分に記載します。

まず、先ほど作成したcircleレイヤーに対してqueryRenderedFeaturesを実行します。queryRenderedFeaturesは指定された地点に存在するフィーチャーを取得します。ここではe.point、つまりクリックした地点のmeasure-pointsレイヤーに関するフィーチャーを取得しています。

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

以下ではGeoJSONの中にフィーチャーが2個以上ある時、features配列の最後から一つ取り除きます。後で出てきますが、GeoJSONのfeaturesは前から順番にPointを詰め込み、最後にLineStringを入れるような処理になっているので、ここではLineStringのデータをGeoJSONから削除しています。

if (geojson.features.length > 1) geojson.features.pop();

距離を表示しているエレメントの中身を空にします。

distanceContainer.innerHTML = '';

queryRenderedFeaturesで取得したfeaturesの中身が「1つ以上ある場合」と「なにもない場合」で場合分けしています。

if (features.length) {
  //1つ以上ある場合
} else {
  //なにもない場合
}

「1つ以上ある場合」というのはクリックした場所にすでに点が描画されている状況です。その場合はその点を削除するため、GeoJSONのfeaturesの中から該当のデータを削除します。

const id = features[0].properties.id;
geojson.features = geojson.features.filter(
  (point) => point.properties.id !== id
);

「なにもない場合」はクリックした点をGeoJSONのfeaturesに追加します。

const point = {
  'type': 'Feature',
  'geometry': {
    'type': 'Point',
    'coordinates': [e.lngLat.lng, e.lngLat.lat]
  },
  'properties': {
    'id': String(new Date().getTime())
  }
};
 
geojson.features.push(point);

GeoJSONのフィーチャーが2個以上のとき、つまり点が2個以上あるときは間に線分を描画し、距離を計測します。

if (geojson.features.length > 1) {
  // 線分を描画する処理
  // 距離を計測して描画する処理   
}

前半は線分を作成しています。Pointの座標からLineStringを作成し、GeoJSONに追加します(geojson.features.push(linestring);)。ここでLineStringを一番最後にpushするので、GeoJSONのfeaturesの中身の最後は必ずLineStringになるのでした。

linestring.geometry.coordinates = geojson.features.map(
  (point) => point.geometry.coordinates
);
 
geojson.features.push(linestring);

後半が距離を計測して描画する処理です。valueは距離を描画するHTMLエレメントです。turf.lengthが今回のメインの処理で、LineStringの距離を返します。最後にvalueの中身を書いて、親コンテナに入れて完成です。

  const value = document.createElement('pre');
  const distance = turf.length(linestring);
  value.textContent = `Total distance: ${distance.toLocaleString()}km`;
  distanceContainer.appendChild(value);

マウス移動時の挙動

マウスカーソルの形を変えています。デフォルトではcrosshair(十字)、点が描画されているところではpointer(指)にカーソルを設定しています。また、点の検出にqueryRenderedFeaturesを使用しています。

map.on('mousemove', (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ['measure-points']
  });
  // Change the cursor to a pointer when hovering over a point on the map.
  // Otherwise cursor is a crosshair.
  map.getCanvas().style.cursor = features.length
    ? 'pointer'
    : 'crosshair';
});

まとめ

Turfを使うと距離の計測が簡単にできることがわかりました。他にも面積を計算したり、円を描画したり、様々な用途に使えます。

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

Discussion