前回、地図データを扱うJavaScript ライブラリ「Leaflet」を使って、GPSログ(GPXファイル)を地図に表示してみた。
今回、この地図に以下の機能の追加をした。サーバーサイドの処理なしで、すべてJavaScriptで行っている。
- 開始地点と終了地点のマーカーにポップアップ表示。
- ポップアップに日時と緯度経度を表示。
- ポップアップに標高を表示。
- 画面右上に、情報ラベル枠を表示。
- 情報ラベルに、出発時間、到着時間、所要時間を表示。
- 情報ラベルに、直線距離、移動距離を表示。
- 情報ラベルに、最高地点、最低地点を表示。
サンプル >>> https://ok2nd.github.io/leaflet/gpx-sample2.html
地図は、「Google マップ」「OpenStreetMap」「国土地理院地図」「Esri World Topo Map」の切り替えができる。
以下、サンプルソース抜粋。
- 開始地点と終了地点のマーカーにポップアップ表示。
- マーカーは「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;
}