12 дек. 2018 г.
Всем привет! Сегодня будем делать карту путешествий с помощью leaflet - показывать на карте много gpx треков. Примеры из статьи доступны на github: https://github.com/antonshell/leaflet_examples.
Задача сама по себе не очень сложная. В прошлом примере мы получали через ajax и отображали на карте один gpx трек. Впринципе можно сделать все то же самое в цикле.
<div id="map"></div>
<script type="text/javascript">
$(document).ready(function() {
createMap('map');
});
function createMap(elementId){
var gpxArray = [
'gpx/austria-2016/full.gpx',
'gpx/greece-2018/full.gpx',
'gpx/italy-coast-to-coast-2017/full.gpx',
'gpx/italy-dolomity-2017/full-2016.gpx',
'gpx/italy-dolomity-2017/full-2017.gpx',
'gpx/italy-dolomity-2017/laguna.gpx',
'gpx/sardegna-2017/full.gpx'
];
var neLat = '36.79923084564507';
var neLng = '8.3107243851';
var swLat = '47.807752154767513';
var swLng = '24.058144837617874';
var map = new L.Map(elementId);
var service = new L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoiYW50b25zaGVsbCIsImEiOiJjam41b3gzZmMwM3V5M2tueHpoanNocnZtIn0.LNyZF8tLB9G-JdW4svni1Q', {
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery � <a href="http://mapbox.com">Mapbox</a>',
maxZoom: 18,
id: 'mapbox.streets'
});
var bounds = new L.LatLngBounds(new L.LatLng(neLat, neLng), new L.LatLng(swLat, swLng));
map.addLayer(service).fitBounds(bounds);
for(var index in gpxArray){
var gpx = gpxArray[index];
$.ajax({
url: gpx,
dataType: "xml",
success: function(xml) {
if(typeof xml != 'object'){
xml = $.parseXML( xml );
}
geojson = toGeoJSON.gpx(xml);
var gjl = L.geoJson(geojson).addTo(map);
}
});
}
}
</script>
Карта отображается. Треки появляются на ней, но не сразу. Треки по очереди загружаются через ajax. Каждый трек может весить несколько мб. В итоге карта может открываться довольно долго и съесть много трафика, в зависимости от количества и размера треков. Также на лету прямо в браузере происходит преобразование gpx в json, что тоже отнимает ресурсы.
Live Demo: http://demo.antonshell.me/leaflet_examples/example_02.html
Это первое, что можно сделать. Если открыть .gpx
файл в блокноте, то мы увидим там обычный xml.
Иногда можно встретить пустые сегменты.
<trkpt lon="29.599016346" lat="60.2472433727">
<ele>23</ele>
<time>2015-07-17T21:09:24Z</time>
<extensions>
<gpxdata:hr></gpxdata:hr>
<gpxdata:cadence></gpxdata:cadence>
<gpxdata:temp></gpxdata:temp>
<power></power>
</extensions>
</trkpt>
Можно их удалить. Это позволит существенно уменьшить размер трека.
<trkpt lon="29.599016346" lat="60.2472433727">
<ele>23</ele>
<time>2015-07-17T21:09:24Z</time>
</trkpt>
Также можно убрать часть значимых данных из трека, которые не нужны для отображения на карте. Например, если на карте не используется время, то можно удалить его. Высоту можно удалить, если не нужна, или округлить до целых. Такие преобразования необратимы, поэтому нужно делать это только для отображения на сайте. Важно сохранить оригинальный трек.
Также можно уменьшить точность координат. Так, по умолчанию gpx трек содержит координаты с 10 знаками после запятой. При этом, 5 знаков после запятой дают погрешность ~1 метр. И скорее всего этого будет достаточно для отображения на карте.
decimal
places degrees distance
------- ------- --------
0 1 111 km
1 0.1 11.1 km
2 0.01 1.11 km
3 0.001 111 m
4 0.0001 11.1 m
5 0.00001 1.11 m
6 0.000001 11.1 cm
7 0.0000001 1.11 cm
8 0.00000001 1.11 mm
9 0.000000001 111 μm
10 0.0000000001 11.1 μm
11 0.00000000001 1.11 μm
12 0.000000000001 111 nm
13 0.0000000000001 11.1 nm
Для отображения на карте Leaflet gpx трек преобразуется в geoJson формат. В предыдущих примерых это происходило на лету, прямо в браузере. Это занимает время и ресурсы клиента. Чтобы ускорить отображение gpx и разгрузить компьютеры пользователей, можно генерировать geoJson на сервере. Для этого можно воспользоваться одной из библиотек. Или же написать свою реализацию и включить туда оптимизации из предыдущих абзацев.
Примеры gpx треков: https://github.com/antonshell/leaflet_examples/tree/master/gpx
<?php
use src\GeoJson;
require '_bootstrap.php';
$geoJsonService = new GeoJson();
$gpxArray = [
__DIR__ . '/gpx/austria-2016/full.gpx',
__DIR__ . '/gpx/greece-2018/full.gpx',
__DIR__ . '/gpx/italy-coast-to-coast-2017/full.gpx',
__DIR__ . '/gpx/italy-dolomity-2017/full-2016.gpx',
__DIR__ . '/gpx/italy-dolomity-2017/full-2017.gpx',
__DIR__ . '/gpx/italy-dolomity-2017/laguna.gpx',
__DIR__ . '/gpx/sardegna-2017/full.gpx'
];
foreach ($gpxArray as $gpx){
echo "Process gpx: " . $gpx . "\n";
$geoJson = $geoJsonService->convertGpxToGeoJson($gpx, true, true, true);
$geoJson = json_encode($geoJson, JSON_UNESCAPED_UNICODE);
$geoJsonGz = gzencode($geoJson);
$dirName = basename(dirname($gpx));
$baseName = basename($gpx);
$fileName = str_replace('.gpx','.geoJson',$baseName);
$fileNameGz = str_replace('.gpx','.geoJson.gz',$baseName);
$dirPath = __DIR__ . '/output/geojson/' . $dirName;
if(!is_dir($dirPath)){
mkdir($dirPath);
}
file_put_contents($dirPath . '/' . $fileName, $geoJson);
file_put_contents($dirPath . '/' . $fileNameGz, $geoJsonGz);
}
echo "Job is done\n";
<?php
namespace src;
/**
* Class GeoJson
* @package src
*/
class GeoJson{
const COORDINATE_PRECISION = 5;
/**
* @param $gpx
* @param bool $reduceCoordinate
* @return array
*/
public function convertGpxToGeoJson($gpx, $reduceCoordinate = false, $removeTime = false, $removeElevation = false)
{
$geoJson = $this->getGeoJsonTemplate();
$coordinates = [];
$coordTimes = [];
$parsedGpx = $this->parseGpx($gpx);
$trkSegments = $parsedGpx['trk']['trkseg'];
// alternative structure
if(isset($trkSegments['trkpt'])){
$trkSegments = [$trkSegments];
}
foreach ($trkSegments as $segmentKey => $segment){
$coordTimes[$segmentKey] = [];
$coordinates[$segmentKey] = [];
if(!isset($segment['trkpt'])){
continue;
}
$trackPoints = $segment['trkpt'];
foreach ($trackPoints as $trackPointKey => $trackPoint){
$elevation = $trackPoint['ele'] ?? '';
$elevation = (string)intval($elevation);
$lat = $trackPoint['@attributes']['lat'];
$lon = $trackPoint['@attributes']['lon'];
$time = $trackPoint['time'] ?? '';
if($reduceCoordinate){
$lat = $this->reduceCoordinate($lat);
$lon = $this->reduceCoordinate($lon);
}
$coordTimes[$segmentKey][$trackPointKey] = $time;
$geoJsonPoint = [
0 => $lon,
1 => $lat,
2 => $elevation,
];
if($removeElevation){
unset($geoJsonPoint[2]);
}
$coordinates[$segmentKey][$trackPointKey] = $geoJsonPoint;
}
}
$geoJson['features'][0]['geometry']['coordinates'] = $coordinates;
$geoJson['features'][0]['properties']['coordTimes'] = $coordTimes;
if($removeTime){
unset($geoJson['features'][0]['properties']['coordTimes']);
}
return $geoJson;
}
/**
* @param $gpx
* @return mixed
*/
private function parseGpx($gpx){
$content = file_get_contents($gpx);
$xml = simplexml_load_string($content);
$json = json_encode($xml);
$parsedGpx = json_decode($json,TRUE);
return $parsedGpx;
}
/**
* @return array
*/
private function getGeoJsonTemplate(){
$geoJson = [
'type' => 'FeatureCollection',
'features' => [
0 => [
'type' => 'Feature',
'geometry' => [
'type' => 'MultiLineString',
'coordinates' => [],
],
'properties' => [
'name' => '',
'time' => '',
'coordTimes' => [],
],
]
]
];
return $geoJson;
}
/**
* @param $coordinate
* @return string
*/
private function reduceCoordinate($coordinate){
$precision = self::COORDINATE_PRECISION;
$coordinate = round($coordinate, $precision);
$coordinate = (string)$coordinate;
return $coordinate;
}
}
В результате получаем json такого вида.
{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"MultiLineString",
"coordinates":[[["22.19879","54.58533","85"],["22.19877","54.58529","84"],
["22.19876","54.58528","84"],["22.19876","54.5852","84"],
["22.19877","54.58512","84"],["22.19899","54.58504","81"],
["22.19893","54.58511","80"],["22.19883","54.58506","78"],
["22.19892","54.58512","76"],["22.19892","54.58512","77"],["22.19895","54.58513","74"]
]]},"properties":{"name":"","time":""}}]}
Запишем в файл .geoJson
и будем запрашивать его с сервера вместо gpx.
В результате удалось уменьшить размер с 3.9 мб до 857 кб.
Сжатие gzip позволяет уменьшить размер еще в несколько раз.
<?php
$gpx = __DIR__ . '/gpx/poland-2018/full.gpx';
$geoJson = $geoJsonService->convertGpxToGeoJson($gpx, true, true, false);
$geoJson = json_encode($geoJson, JSON_UNESCAPED_UNICODE);
$geoJsonGz = gzencode($geoJson);
$dirPath = __DIR__ . '/output/geojson/poland-2018';
file_put_contents($dirPath . '/full.geoJson.gz', $geoJsonGz);
Чтобы браузер корректно обрабатывал gzip и автоматиески извлекал его
нужно передать заголовок header('Content-Encoding: gzip');
Для этого нужно специально настроить сервер(рекомендуется).
Либо можно написать простенький скрипт, который будет возвращать файл и соответствующий заголовок.
var geoJsonUrl = 'output/full.php?file=poland-2018/full.geoJson.gz';
<?php
// full.php
$file = $_GET['file'] ?? '';
$dirName = basename(dirname($file));
$baseName = basename($file);
$filePath = __DIR__ . '/geojson/' . $dirName . '/' . $baseName;
$data = file_get_contents($filePath);
header("Content-type: application/json");
header('Content-Encoding: gzip');
echo $data;
В результате получаем такую статистику для одного трека:
Gpx - 3,70M: full.gpx
Geojson + оптимизации - 837,01K: full.geoJson
Geojson + оптимизации + gzip - 153,51K: full.geoJson.gz
Обычно gpx треки берутся с gpx навигатора. Для пешеходных и велосипедных путешествий скорее всего это будет что-то из линейки Garmin Etrex. Этот навигатор обычно пишет треки по дням. В результате, для путешествия получаем примерно такой набор треков.
Пример файлов: https://github.com/antonshell/leaflet_examples/tree/master/gpx/5-capitals
2014-08-08_20-35-01_Day.gpx
2014-08-09_10-24-16_Day.gpx
2014-08-10_09-37-39_Day.gpx
2014-08-11_00-01-24_Day.gpx
2014-08-12_09-50-25_Day.gpx
2014-08-13_00-00-39_Day.gpx
2014-08-15_08-53-16_Day.gpx
2014-08-16_13-45-55_Day.gpx
2014-08-17_08-25-25_Day.gpx
2014-08-18_17-49-08_Day.gpx
2014-08-19_14-46-34_Day.gpx
2014-08-20_09-38-14_Day.gpx
2014-08-21_12-50-30_Day.gpx
2014-08-24_08-21-12_Day.gpx
2014-08-25_08-38-40_Day.gpx
2014-08-26_11-45-51_Day.gpx
2014-08-27_09-13-32_Day.gpx
Для отображения на карте удобнее ипользовать единый трек, например такой: 5-capitals-full.gpx
.
Для объединения(конкатенации) треков можно использовать сторонний сервис: https://gotoes.org/strava/Combine_GPX_TCX_FIT_Files.php
Или можно написать свой скрипт, который будет делать примерно то же самое.
<?php
use src\GpxCombine;
require '_bootstrap.php';
$combineService = new GpxCombine();
$gpxArray = [
__DIR__ . '/gpx/5-capitals/2014-08-08_20-35-01_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-09_10-24-16_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-10_09-37-39_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-11_00-01-24_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-12_09-50-25_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-13_00-00-39_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-15_08-53-16_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-16_13-45-55_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-17_08-25-25_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-18_17-49-08_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-19_14-46-34_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-20_09-38-14_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-21_12-50-30_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-24_08-21-12_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-25_08-38-40_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-26_11-45-51_Day.gpx',
__DIR__ . '/gpx/5-capitals/2014-08-27_09-13-32_Day.gpx',
];
$gpxContent = $combineService->combineGpxArray($gpxArray);
file_put_contents(__DIR__ . '/output/combined.gpx', $gpxContent);
echo "Job is done\n";
<?php
namespace src;
/**
* Class GpxCombine
* @package src
*/
class GpxCombine{
/**
* @param $gpxArray
* @return string
*/
public function combineGpxArray($gpxArray)
{
$trkSegments = [];
foreach ($gpxArray as $gpx) {
$parsedGpx = $this->parseGpx($gpx);
if(is_array($parsedGpx['trk']['trkseg']) && count($parsedGpx['trk']['trkseg'])){
$trkSegments = array_merge($trkSegments, $parsedGpx['trk']['trkseg']);
}
}
$content = '';
foreach ($trkSegments as $i1 => $trkSegment){
$segmentContent = '';
$segmentContent .= "\t<trkseg>\n";
foreach ($trkSegment['trkpt'] as $i2 => $trkpt){
if(!isset($trkpt['@attributes'])){
continue;
}
$lat = $trkpt['@attributes']['lat'];
$lon = $trkpt['@attributes']['lon'];
$elevation = $trkpt['ele'];
$time = $trkpt['time'];
$trkptContent = '';
$trkptContent .= "\t\t" . '<trkpt lat="' . $lat . '" lon="' . $lon . '">' . "\n";
$trkptContent .= "\t\t\t" . '<ele>' . $elevation . '</ele>' . "\n";
$trkptContent .= "\t\t\t" . '<time>' . $time . '</time>' . "\n";
$trkptContent .= "\t\t" . '</trkpt>' . "\n";
/*$trkptContent = "\t" . '<trkpt lat="' . $lat . '" lon="' . $lon . '">
<ele>' . $elevation . '</ele>
<time>' . $time . '</time>
</trkpt>';*/
$segmentContent .= $trkptContent;
}
$segmentContent .= "\t</trkseg>\n";
$content .= $segmentContent;
}
$gpxTemplate = file_get_contents(__DIR__ . '/gpx_template.gpx');
$gpxTemplate = str_replace('{{{content}}}', $content, $gpxTemplate);
return $gpxTemplate;
}
/**
* @param $gpx
* @return mixed
*/
private function parseGpx($gpx){
$content = file_get_contents($gpx);
$xml = simplexml_load_string($content);
$json = json_encode($xml);
$parsedGpx = json_decode($json,TRUE);
return $parsedGpx;
}
}
Для привязки карты используем метод из предыдущей статьи. Но на этот раз нам нужно получить максимальные и минимальные координаты для всех имеющихся треков. Иногда это может быть не нужно, Например, если треки разбросаны по всему миру. В этом случае можно исключить определенные треки
<?php
use src\GpxBounds;
require '_bootstrap.php';
$boundsService = new GpxBounds();
$gpxArray = [
__DIR__ . '/gpx/austria-2016/full.gpx',
__DIR__ . '/gpx/greece-2018/full.gpx',
__DIR__ . '/gpx/italy-coast-to-coast-2017/full.gpx',
__DIR__ . '/gpx/italy-dolomity-2017/full-2016.gpx',
__DIR__ . '/gpx/italy-dolomity-2017/full-2017.gpx',
__DIR__ . '/gpx/italy-dolomity-2017/laguna.gpx',
__DIR__ . '/gpx/sardegna-2017/full.gpx'
];
$bounds = $boundsService->getLatLngBoundsArray($gpxArray);
print_r($gpxArray);
print_r($bounds);
<?php
namespace src;
use SimpleXMLElement;
/**
* Class GpxBounds
* @package src
*/
class GpxBounds{
/**
* @param $gpx
* @return array
*/
public function getLatLngBounds($gpx){
$latArray = [];
$lonArray = [];
$parsedGpx = $this->parseGpx($gpx);
foreach($parsedGpx as $line){
if(isset($line['attributes']['LAT'])){
$latArray[] = $line['attributes']['LAT'];
}
if(isset($line['attributes']['LAT'])){
$lonArray[] = $line['attributes']['LON'];
}
}
$bounds = [
'neLat' => min($latArray),
'neLng' => min($lonArray),
'swLat' => max($latArray),
'swLng' => max($lonArray),
];
return $bounds;
}
/**
* @param $gpxArray
* @return array
*/
public function getLatLngBoundsArray($gpxArray)
{
$latArray = [];
$lonArray = [];
foreach ($gpxArray as $gpx){
$parsedGpx = $this->parseGpx($gpx);
foreach($parsedGpx as $line){
if(isset($line['attributes']['LAT'])){
$latArray[] = $line['attributes']['LAT'];
}
if(isset($line['attributes']['LAT'])){
$lonArray[] = $line['attributes']['LON'];
}
}
}
$bounds = [
'neLat' => min($latArray),
'neLng' => min($lonArray),
'swLat' => max($latArray),
'swLng' => max($lonArray),
];
return $bounds;
}
/**
* @param $gpx
* @return mixed
*/
private function parseGpx($gpx){
$content = file_get_contents($gpx);
$parser = xml_parser_create();
xml_parse_into_struct($parser,$content,$parsedGpx);
return $parsedGpx;
}
}
В результате получаем координаты привязки:
Array
(
[neLat] => 36.79923084564507
[neLng] => 8.3107243851
[swLat] => 47.807752154767513
[swLng] => 24.058144837617874
)
В результате получилась такая карта путешествий.
На ней поместилось несколько больших треков и открывается она быстро. Примеры доступены на github: https://github.com/antonshell/leaflet_examples
Карта моих путешествий выглядит так и дополняется новыми треками: https://velocrunch.ru/gpx/general-map
На этом пока все, спасибо за внимание!