Mapbox Newsletter WEEKLY TIPSの解説 -「距離を測定」
はじめに
この記事は、先日配信されたMapbox NewsletterのWEEKLY TIPSで紹介されていた「距離を測定」についての解説です。このサンプルはturf.lineDistanceの使い方について例示しています。また、Newsletterの購読はこちらからお申し込みいただけます。
以下が本サンプルのデモです。地図上をクリックしていくと点が打たれ、点と点の間に線分が描画されます。また、点と点の間の距離が表示されます。点をクリックするとその点を消せます。
Turf
Turfは地理情報の分析等を行うJavaScriptのライブラリです。Mapbox GL JSと組み合わせて使うことができます。
今回使用するlength
のドキュメントはこちらです。使い方は以下の通り、引数にLineString
を含むGeoJSONを入れるだけです(turf.lineString
はLineString
を含む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ソースを作成します。最初はgeojson
のfeatures
が空配列なので、GeoJSONとしては空データになります。
map.addSource('geojson', {
'type': 'geojson',
'data': geojson
});
1つ目のレイヤーはクリック時に表示される点を描画するcircleレイヤーです。レイヤーidはmeasure-points
です。ソースは先程のgeojson
を使用します。filter
のin
はフィーチャー(ここではGeoJSON)の中から条件にマッチするものだけを返すフィルタ表現です。第二引数はフィーチャーのプロパティ名か、$type
/$id
を指定します。第三引数はマッチする値を指定します。$type
の場合は第三引数にPoint
、LineString
または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を使うと距離の計測が簡単にできることがわかりました。他にも面積を計算したり、円を描画したり、様々な用途に使えます。
Discussion