晴歩雨描

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

LeafletでGPSログ(GPX)地図:標高グラフ(Highcharts)追加。

地図データを扱うJavaScript ライブラリ「Leaflet」を使って、GPSログ(GPXファイル)の地図表示を試している。

この地図に、標高グラフを追加した。

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

f:id:art2nd:20210508153058j:plain

グラフ作成には、「Highcharts」を使用。

「highcharts.js」を読み込む。

<script src="//code.highcharts.com/highcharts.js"></script>

GPXから読み込んだ時間データと標高データを使う。

<script>
・・・・・
var chartEle = [];
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;
	chartEle[i] = [pos['time'].getTime() + 60*60*9*1000, parseInt(height)];	// 日本時間
}
var subtitle = start['timeStr'] + '~' + end['timeStr'] + ' 所要時間:' + diffTime
	+ ' 距離:' + distTotalKm + 'km 最高地点:' + Math.round(height_max) + 'm 最低地点:' + Math.round(height_min) + 'm';
chartView(chartEle, subtitle);
function chartView(chartEle, subtitle) {
chart = new Highcharts.Chart({
	chart: {
		renderTo: 'chart',
		zoomType: 'xy'
	},
	title: {
		text: '標高 グラフ',
		style: {
			fontWeight: 'bold'
		}
	},
	subtitle: {
		text: subtitle
	},
	xAxis: {
		type: 'datetime'
	},
	yAxis: [{ // Primary yAxis
		title: {
			text: '標高',
			style: {
				color: '#89A54E'
			}
		},
		labels: {
			formatter: function() {
				return this.value +' m';
			},
			style: {
				color: '#89A54E'
			}
		}
	}],
	tooltip: {
		formatter: function() {
			var dt = Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x);
			var unit = {
				'標高': 'm'
			}[this.series.name];
			return '<b>' + dt +'</b><br><b>'+ this.y + '</b> ' + unit;
		}
	},
	legend: {
		layout: 'vertical',
		align: 'left',
		verticalAlign: 'top',
		floating: true,
		backgroundColor: '#FFFFFF'
	},
	plotOptions: {
		area: {
			fillColor: {
				linearGradient: {x1:0, y1:0, x2:0, y2:1},
				stops: [
					[0, '#89A54E'],
					[1, 'rgba(0,30,0,0)']
				]
			},
			lineWidth: 1,
			marker: {
				enabled: false,
				states: {
					hover: {
						enabled: true,
						radius: 5
					}
				}
			},
			shadow: false,
			states: {
				hover: {
					lineWidth: 1
				}
			},
			threshold: null
		},
		spline: {
			lineWidth: 1,
			marker: {
				enabled: false,
			}
		}
	},
	series: [{
		name: '標高',
		color: '#89A54E',
		type: 'area',
		data: chartEle
	}]
});
}
</script>

ブラウザの画面サイズの変化に合わせて、地図の高さを変えるようにした。

<div id="map"></div>
<div id="chart"></div>
・・・・・
<script>
winResize();
window.addEventListener('DOMContentLoaded', function(){
	window.addEventListener('resize', function(){
		winResize();
	});
});
function winResize() {
	let h = document.documentElement.clientHeight;
	document.getElementById('map').style.height = parseInt(h) - 240 + 'px';
}
</script>

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;
}

JR福知山線)道場駅近くの武庫川沿い。オオヨシキリ、ウグイス、セグロセキレイ(+幼鳥)、ハクセキレイ、ホオジロ、モズ、ツバメ、コゲラ、ムクドリ、カワラヒワ、カルガモ、アオサギ、カワウ。

5月6日。JR福知山線道場駅→千苅(千刈)ダム→生野橋→武庫川沿い→道場駅。8.9km、3時間30分。

f:id:art2nd:20210506215158j:plain

野鳥では、オオヨシキリを初めて撮影。

生野橋を渡った後の武庫川沿いで、オオヨシキリのさえずりが一面に鳴り響いていた。独特の複雑なさえずり。多くは姿を見せないが、時々ヨシ(?)に混じって生えている木の枝にとまって鳴いていた。

オオヨシキリに混じって負けじとウグイスも鳴いていた。ウグイスも珍しく複数の個体を撮影できた。

もう一羽、見慣れない鳥が撮影できたが、セグロセキレイの幼鳥か?。普通セキレイ系は歩き回ってじっとしていないが、この鳥は長いこと欄干の上でじっとしていた。

他には、ハクセキレイホオジロ、モズ、ツバメ、コゲラムクドリカワラヒワカルガモアオサギ、カワウ。

f:id:art2nd:20210506222403j:plain

オオヨシキリ

f:id:art2nd:20210506222439j:plain

オオヨシキリ

f:id:art2nd:20210506222639j:plain

ウグイス

f:id:art2nd:20210506222750j:plain

ウグイス

f:id:art2nd:20210506222759j:plain

ウグイス

f:id:art2nd:20210506222903j:plain

セグロセキレイの幼鳥?

f:id:art2nd:20210506222941j:plain

セグロセキレイ

f:id:art2nd:20210506223006j:plain

ハクセキレイ

f:id:art2nd:20210506223127j:plain

ホオジロ

f:id:art2nd:20210506223032j:plain

モズ

f:id:art2nd:20210506223203j:plain

ツバメ

f:id:art2nd:20210506223212j:plain

ツバメ

f:id:art2nd:20210506223233j:plain

コゲラ

f:id:art2nd:20210506223446j:plain

ムクドリ

f:id:art2nd:20210506223252j:plain

カワラヒワ

f:id:art2nd:20210506223321j:plain

カルガモ

f:id:art2nd:20210506223337j:plain

アオサギ

f:id:art2nd:20210506223400j:plain

カワウ

JR福知山線)道場駅→千苅(千刈)ダム→武庫川沿い。

5月6日。JR福知山線道場駅→千苅(千刈)ダム→生野橋→武庫川沿い→道場駅。8.9km、3時間30分。

千苅ダムは、大正8年(1919年)建設のレトロ感漂うダム。2019年1月にも訪れたが、その時は放流していなかった。今回は見事な水量の放流。横にある滝の水量もすごい。

生野橋を渡った後の武庫川沿いでは、オオヨシキリのさえずりが凄かった。野鳥写真は続きのブログに(↓)。

f:id:art2nd:20210506215158j:plain

f:id:art2nd:20210506215423j:plain

f:id:art2nd:20210506215428j:plain

f:id:art2nd:20210506215438j:plain

f:id:art2nd:20210506215442j:plain

f:id:art2nd:20210506215637j:plain

f:id:art2nd:20210506215617j:plain

f:id:art2nd:20210506215717j:plain

f:id:art2nd:20210506215725j:plain

f:id:art2nd:20210506215732j:plain

f:id:art2nd:20210506215807j:plain

f:id:art2nd:20210506215811j:plain

f:id:art2nd:20210506215816j:plain

f:id:art2nd:20210506215821j:plain

f:id:art2nd:20210506215825j:plain

f:id:art2nd:20210506215829j:plain

f:id:art2nd:20210506215833j:plain

Leafletで、GPSログ(GPX)を地図に表示する。Google マップ、OpenStreetMap、国土地理院地図、Esri World Topo Map。

地図データを扱うJavaScript ライブラリ「Leaflet」を使って、地図表示を試してきた。

今回、GPSログ(GPXファイル)を地図に表示してみた。参考にしたのは以下のページ。

「Leaflet」のプラグインleaflet-gpx」を使う。

まず、「leaflet-gpx」のJavaScriptファイルを読み込む。

<script src="//cdnjs.cloudflare.com/ajax/libs/leaflet-gpx/1.5.1/gpx.min.js"></script>

GPSログを表示するだけなら、以下のようにGPXファイルを指定して、L.GPXを呼び出すだけ。ただ、これだと開始地点と終了地点のマーカーが大きすぎるのと経路の線の色がきつい。

var gpx = 'gpx/2021_05_04_10_34_23.gpx';
new L.GPX(gpx, {async: true}).on('loaded', function(e) {
	map.fitBounds(e.target.getBounds());
}).addTo(map);

f:id:art2nd:20210505230009j:plain

マーカーと線の色を変えてみた。

var gpx = 'gpx/2021_05_04_10_34_23.gpx';
new L.GPX(gpx, {
	async: true,
	marker_options: {
		startIconUrl: 'icon/ltblue-dot.png',
		endIconUrl: 'icon/red-dot.png',
		shadowUrl: false,
		iconSize: [32, 32],
		iconAnchor: [16, 30]
	},
	polyline_options: {
		color: '#3B83CC',
		opacity: 0.8,
		weight: 5,
		lineCap: 'round'
    	}
}).on('loaded', function(e) {
	map.fitBounds(e.target.getBounds());
}).addTo(map);

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

f:id:art2nd:20210514112143j:plain

下は、GoogleマイマップにGPXをインポートした例。

f:id:art2nd:20210504170125j:plain

(↓)機能追加。