前回、地図データを扱う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; }