晴歩雨描

晴れた日は外に出て歩き、雨の日は部屋で絵を描く

LeafletでGPSログ(GPX)地図:移動距離、所要時間、出発時間、到着時間、最高地点、最低地点表示。Google マップ、OpenStreetMap、国土地理院地図、Esri World Topo Map。

前回、地図データを扱うJavaScript ライブラリ「Leaflet」を使って、GPSログ(GPXファイル)を地図に表示してみた。

今回、この地図に以下の機能の追加をした。サーバーサイドの処理なしで、すべてJavaScriptで行っている。

  • 開始地点と終了地点のマーカーにポップアップ表示。
  • ポップアップに日時と緯度経度を表示。
  • ポップアップに標高を表示。
  • 画面右上に、情報ラベル枠を表示。
  • 情報ラベルに、出発時間、到着時間、所要時間を表示。
  • 情報ラベルに、直線距離、移動距離を表示。
  • 情報ラベルに、最高地点、最低地点を表示。
f:id:art2nd:20210508100145j:plain
f:id:art2nd:20210508100142j:plain

サンプル >>> https://ok2nd.github.io/leaflet/gpx-sample2.html

f:id:art2nd:20210514113804j:plain

地図は、「Google マップ」「OpenStreetMap」「国土地理院地図」「Esri World Topo Map」の切り替えができる。

f:id:art2nd:20210514113837j:plain

f:id:art2nd:20210514113840j:plain

f:id:art2nd:20210514113843j:plain

以下、サンプルソース抜粋。

  • 開始地点と終了地点のマーカーにポップアップ表示。
  • マーカーは「leaflet-gpx」のマーカーは使わずに、L.markerでマーカー表示。
  • GPXファイルをXMLHttpRequest()で読み込む。
  • GPXファイルから、日時、緯度経度、標高を取得する関数 gpxParse(trkpt) を作成。
  • ポップアップに日時と緯度経度を表示。
  • ポップアップに標高を表示。
var gpxFile = 'gpx/2021_05_04_10_34_23.gpx';
new L.GPX(gpxFile, {
	async: true,
	marker_options: {
		startIconUrl: false,
		endIconUrl: false,
		shadowUrl: false
	},
	polyline_options: {
		color: '#3B83CC',
		opacity: 0.8,
		weight: 5,
		lineCap: 'round'
	}
}).on('loaded', function(e) {
	map.fitBounds(e.target.getBounds());
}).addTo(map);
// ---------------------------------------------------
var iconStart = L.icon({
	iconUrl: 'icon/ltblue-dot.png',
	iconRetinaUrl: 'icon/ltblue-dot.png',
	iconSize: [32, 32],
	iconAnchor: [16, 30],
	popupAnchor: [1, -22],
});
var iconEnd = L.icon({
	iconUrl: 'icon/red-dot.png',
	iconRetinaUrl: 'icon/red-dot.png',
	iconSize: [32, 32],
	iconAnchor: [16, 30],
	popupAnchor: [1, -22],
});
var request = new XMLHttpRequest();
request.open('get', gpxFile, false);
request.send(null);
var gpxStr = request.responseText;
var parser = new DOMParser();
var gpx = parser.parseFromString(gpxStr, 'text/xml');
var elements = gpx.getElementsByTagName('trkpt');
var startPoint = elements.item(0);
var endPoint = elements.item(elements.length-1);
// ---------------------------------------------------
var start = gpxParse(startPoint);
posStr1 = '<span class="panel"><strong>【 開始地点 】</strong><br>'
	+ start['dateStr'] + ' ' + start['timeStr'] + '<br>'
	+ '緯度:' + start['lat'] + '<br>'
	+ '経度:' + start['lon'] + '<br>'
	+ '標高:' + start['ele'] + ' m</span>';
L.marker([start['lat'] , start['lon'] ], {icon: iconStart}).addTo(map).bindPopup(posStr1);
// ---------------------------------------------------
var end = gpxParse(endPoint);
posStr2 = '<span class="panel"><strong>【 終了地点 】</strong><br>'
	+ end['dateStr'] + ' ' + end['timeStr'] + '<br>'
	+ '緯度:' + end['lat'] + '<br>'
	+ '経度:' + end['lon'] + '<br>'
	+ '標高:' + end['ele'] + ' m</span>';
L.marker([end['lat'] , end['lon'] ], {icon: iconEnd}).addTo(map).bindPopup(posStr2);
// ---------------------------------------------------
function gpxParse(trkpt) {
	var timeTxt = trkpt.getElementsByTagName('time')[0].textContent;
	var time = new Date(timeTxt);
	return {
		lat: parseFloat(trkpt.getAttribute('lat')),
		lon: parseFloat(trkpt.getAttribute('lon')),
		time: time,
		dateStr: time.toLocaleDateString(),
		timeStr: time.toLocaleTimeString(),
		ele: trkpt.getElementsByTagName('ele')[0].textContent
	};
}
  • 画面右上に、情報ラベル枠を表示
  • 情報ラベルに、出発時間、到着時間、所要時間を表示
  • 情報ラベルに、直線距離、移動距離を表示
  • 情報ラベルに、最高地点、最低地点を表示

画面右上の情報ラベル枠表示で参考にしたのは以下のページ。

var distTotal = 0;
var before = {};
var height_max = -10000;
var height_min = 10000;
for (var i=0; i<(elements.length); i++) {
	let pos = gpxParse(elements.item(i));
	if (i > 0) {
		let before = gpxParse(elements.item(i-1));
		distTotal += distance(before['lat'], before['lon'], pos['lat'], pos['lon'], false);
	}
	height = parseFloat(pos['ele']);
	if (height_max < height) height_max = height;
	if (height_min > height) height_min = height;
}
var diffTime = time2str(end['time'].getTime() - start['time'].getTime());
var distTotalKm = Math.round(distTotal/1000 * 1000) / 1000;	// 小数第三位で四捨五入
var dist = distance(start['lat'], start['lon'], end['lat'], end['lon'], false);
var distKm = Math.round(dist/1000 * 1000) / 1000;	// 小数第三位で四捨五入
panelText = '<span class="panelDate">' + start['dateStr'] + '</span><br>'
		 + '出発時間:' + start['timeStr'] + '<br>'
		 + '到着時間:' + end['timeStr'] + '<br>'
		 + '所要時間:' + diffTime + '<br>'
		 + '直線距離:' + distKm + ' km<br>'
		 + '移動距離:' + distTotalKm + ' km<br>'
		 + '最高地点:' + Math.round(height_max) + ' m<br>'
		 + '最低地点:' + Math.round(height_min) + ' m<br>';
// ---------------------------------------------------
L.CustomControl = L.Control.extend({
	onAdd: function(map) {
		this._div = L.DomUtil.create('div', 'panel leaflet-bar');
		return this._div;
	},
	setContent: function(latlng) {
		latlng = latlng.wrap()
		this._div.innerHTML = '<pre class="panel">' + panelText + '</pre>';
		return this;
	}
});
L.customControl = function(opts) {
	return new L.CustomControl(opts);
}
const dmy = L.latLng(34.69464402144777, 135.19480347633365);
L.customControl({position: 'topright'}).addTo(map).setContent(dmy);
// ---------------------------------------------------
function time2str(time) {
	var timeHour = time / (1000 * 60 * 60);
	var timeMinute = (timeHour - Math.floor(timeHour)) * 60;
	var timeSecond = (timeMinute - Math.floor(timeMinute)) * 60;
	return ('00' + Math.floor(timeHour)).slice(-2) + ':' + ('00' + Math.floor(timeMinute)).slice(-2) + ':' + ('00' + Math.round(timeSecond)).slice(-2);
}

2地点間の距離の計算をする関数 distance()。2地点間の距離の計算は以下のページを参考にした。「ヒュベニの公式」を採用。

function distance(lat1, lon1, lat2, lon2, mode=true) {
	// 緯度経度をラジアンに変換
	radLat1 = lat1 * (Math.PI / 180);
	radLon1 = lon1 * (Math.PI / 180);
	radLat2 = lat2 * (Math.PI / 180);
	radLon2 = lon2 * (Math.PI / 180);
	// 緯度差
	radLatDiff = radLat1 - radLat2;
	// 経度差算
	radLonDiff = radLon1 - radLon2;
	// 平均緯度
	radLatAve = (radLat1 + radLat2) / 2.0;
	// 測地系による値の違い
	a = mode ? 6378137.0 : 6377397.155; // 赤道半径
	b = mode ? 6356752.314140356 : 6356078.963; // 極半径
	//e2 = (a*a - b*b) / (a*a);
	e2 = mode ? 0.00669438002301188 : 0.00667436061028297; // 第一離心率^2
	//a1e2 = a * (1 - e2);
	a1e2 = mode ? 6335439.32708317 : 6334832.10663254; // 赤道上の子午線曲率半径
	sinLat = Math.sin(radLatAve);
	W2 = 1.0 - e2 * (sinLat*sinLat);
	M = a1e2 / (Math.sqrt(W2)*W2); // 子午線曲率半径M
	N = a / Math.sqrt(W2); // 卯酉線曲率半径
	t1 = M * radLatDiff;
	t2 = N * Math.cos(radLatAve) * radLonDiff;
	dist = Math.sqrt((t1*t1) + (t2*t2));
	return dist;
}