Leaflet - базовая настройка карты + gpx

20 нояб. 2018 г.

Всем привет! Сегодня раскажу о работе с библиотекой для отображения карт Leaflet. Примеры из статьи доступны на github: https://github.com/antonshell/leaflet_examples. Библитека может быть полезна, например, для отображения карт на сайте. В частности для отображения gpx треков. Преимущество перед google/yandex maps в том, что leaflet - это открытая библиотека и предоставляет больше возможностей для настройки/расширения. Главный недостаток в том, что придется настраивать все самому. Из коробки может выглядеть не так красиво, как google maps, зато есть множество возможностей расширения функционала.

Установка Leaflet

Для работы нам понадобится d3, leaflet, плагины leaflet-elevation и leaflet-gpx, а также togeojson. Для работы одного из примеров нужен пакет xmldom. Также в примере используется jquery, хотя непосредственно для работы карт она не требуется.

Есть примеры работы gpx и elevation плагинов https://github.com/MrMufflon/Leaflet.Elevation http://mpetazzoni.github.io/leaflet-gpx/

Можно установить зависимости через npm.

npm install jquery
npm install d3
npm install leaflet
npm install leaflet.elevation
npm install leaflet-gpx
npm install @mapbox/togeojson

npm install xmldom

Правда, в этом случае возможна проблема с версиями пакетов. В некоторых случаях может отвалиться, например, отображение высоты.

В данном примере для наглядности буду использовать строго определенные версии пакетов.

  • d3 - 3.5.12
  • leaflet - 0.7.2
  • leaflet-elevation - 0.0.1
  • leaflet-elevation - версия от 16.aug.2015
  • togeojson - 0.13.0

Подключаем скрипты и css на страницу. В данном примере использую файлы из папки lib.

<script src="lib/jquery/dist/jquery.min.js"></script>

<script charset="utf-8" src="lib/d3/d3.min.js"></script>

<link href="css/gpx.css" rel="stylesheet">

<link href="lib/leaflet/leaflet.css" rel="stylesheet">

<link href="lib/leaflet-elevation/dist/Leaflet.Elevation-0.0.2.css" rel="stylesheet">

<script src="lib/leaflet/leaflet.js" type="text/javascript"></script>
<script src="lib/leaflet-elevation/dist/Leaflet.Elevation-0.0.2.min.js" type="text/javascript"></script>
<script src="lib/leaflet-gpx/gpx.js" type="text/javascript"></script>

<script src='lib/togeojson/togeojson.js'></script>

Отображение карты

Подключаем слой карты. Для этого нужно зарегестрироваться на сервисе mapbox https://www.mapbox.com/account/ и получить token. Бесплатный аккаунт включает x показов карт в месяц.

Теперь можем отобразить карту на сайте. В качестве слоя карт используется open street maps.

<div id="map"></div>

<script type="text/javascript">

    $(document).ready(function() {
        createMap('map');
    });

    function createMap(elementId){
        var neLat="36.79923084564507";
        var neLng="21.265787724405527";
        var swLat="38.496870584785938";
        var swLng="24.058144837617874";

        var gpx = 'gpx/greece-2018/full.gpx';

        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 &copy; <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);
    }
</script>

Подключение gpx

Попробуем добавить gpx трек на сайт. Будем запрашивать его с сервера по ajax. Также включим отображение высоты.

Live Demo: http://demo.antonshell.me/leaflet_examples/example_01.html

<div id="map"></div>

<script type="text/javascript">

    $(document).ready(function() {
        createMap('map');
    });

    function createMap(elementId){
        var neLat="36.79923084564507";
        var neLng="21.265787724405527";
        var swLat="38.496870584785938";
        var swLng="24.058144837617874";

        var gpx = 'gpx/greece-2018/full.gpx';

        $.ajax({
            url: gpx,
            dataType: "xml",
            success: function(xml) {
                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 &copy; <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));

                if(typeof xml != 'object'){
                    xml = $.parseXML( xml );
                }

                geojson = toGeoJSON.gpx(xml);

                var el = L.control.elevation({
                    width: 400,
                    height: 125
                });
                el.addTo(map);

                var gjl = L.geoJson(geojson,{
                    onEachFeature: el.addData.bind(el)
                }).addTo(map);

                map.addLayer(service).fitBounds(bounds);
            }
        });
    }
</script>

Привязка карты

Чтобы трек gpx трек отображадся в центре карты, нужно настроить привязку карты. Для этого нам нужно получить максимальные и минимальный значения широты и долготы. Иногда эта информация содержится в метаданных трека. Если нет, то c помощью php скрипты можно обойти все координаты и найти максимальные и минимальные значения.

<?php

use src\GpxBounds;

require '_bootstrap.php';

$boundsService = new GpxBounds();

$gpx = __DIR__ . '/gpx/greece-2018/full.gpx';
$bounds = $boundsService->getLatLngBounds($gpx);

echo $gpx . "\n";
print_r($bounds);
<?php

// $gpx = 'gpx/greece-2018/full.gpx';

namespace src;

use SimpleXMLElement;

/**
 * Class GpxBounds
 * @package src
 */
class GpxBounds{

    /**   
     * @param $gpx // example: 'gpx/greece-2018/full.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;
    }
}
// $gpx = 'gpx/greece-2018/full.gpx';

/**
 * @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 $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;
}

В результате получаем массив координат и можем использовать их в leaflet для привязки карты

Array
(
    [neLat] => 36.79923084564507
    [neLng] => 21.265787724405527
    [swLat] => 38.496870584785938
    [swLng] => 24.058144837617874
)

Отображение маркеров

Отображение маркеров на карте делается достаточно просто. Достаточно создать объект, для каждого маркера, указать текст и привязать его к карте.

Live Demo: http://demo.antonshell.me/leaflet_examples/example_06.html

var marker1 = L.marker([36.99923084564507, 21.995787724405527]).addTo(map);
marker1.bindPopup("<b>Marker 1</b><br>I am a popup.");

var marker2 = L.marker([37.22923084564507, 22.595787724405527]).addTo(map);
marker2.bindPopup("<b>Marker 2</b><br>I am a popup.");

Отображение маршрутных точек Garmin

Маршрутные точки garmin хранятся в файлах в формате gpx. Одна точка выглядит примерно так:

<wpt lat="54.214897966012359" lon="22.155264979228377">
    <ele>137.40441899999999</ele>
    <time>2018-08-12T11:21:16Z</time>
    <name>0555</name>
    <sym>Flag, Blue</sym>
    <type>user</type>
    <extensions>
      <gpxx:WaypointExtension>
        <gpxx:DisplayMode>SymbolAndName</gpxx:DisplayMode>
      </gpxx:WaypointExtension>
      <wptx1:WaypointExtension>
        <wptx1:DisplayMode>SymbolAndName</wptx1:DisplayMode>
      </wptx1:WaypointExtension>
      <ctx:CreationTimeExtension>
        <ctx:CreationTime>2018-08-12T11:21:16Z</ctx:CreationTime>
      </ctx:CreationTimeExtension>
    </extensions>
  </wpt>

Здесь нам нужны координаты, название точки и, возможно, время. На сервере с помощью php скрипта обработаем gpx файл, вытащим оттуда все данные и сохраним в json.

<?php

use src\GeoJson;
use src\TrackPoints;

require '_bootstrap.php';

$geoJsonService = new GeoJson();
$trackpointsService = new TrackPoints();

// gpx to geojson
$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';
if(!is_dir($dirPath)){
    mkdir($dirPath);
}

file_put_contents($dirPath . '/full.geoJson', $geoJson);
file_put_contents($dirPath . '/full.geoJson.gz', $geoJsonGz);

// trackPoints
$gpx = __DIR__ . '/gpx/poland-2018/points.gpx';
$trackPoints = $trackpointsService->parseGpxTrackPoints($gpx);
$trackPoints = json_encode($trackPoints, JSON_UNESCAPED_UNICODE);
$trackPointsGz = gzencode($trackPoints);

file_put_contents($dirPath . '/trackPoints.json', $trackPoints);
file_put_contents($dirPath . '/trackPoints.json.gz', $trackPointsGz);

echo "Job is done\n";
<?php

namespace src;

/**
 * Class TrackPoints
 * @package src
 */
class TrackPoints{
    /**
     * @param $gpx
     * @return array
     */
    public function parseGpxTrackPoints($gpx){
        $parsedGpx = $this->parseGpx($gpx);
        $data = ['bounds' => [], 'points' => []];

        if(isset($parsedGpx['metadata']['bounds']['@attributes'])){
            $data['bounds'] = $parsedGpx['metadata']['bounds']['@attributes'];
        }

        $points = [];
        foreach ($parsedGpx['wpt'] as $item){
            $points[] = [
                'lat' => $item['@attributes']['lat'],
                'lon' => $item['@attributes']['lon'],
                'elevation' => $item['ele'] ?? '',
                'time' => $item['time'] ?? '',
                'name' => $item['name'] ?? 'No name',
            ];
        }

        $data['points'] = $points;
        return $data;
    }

    /**
     * @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;
    }
}

Затем в бразуере через ajax получим json файл и

var trackPointsUrl = 'output/full.php?file=poland-2018/trackPoints.json.gz';

// track points
$.ajax({
    url: trackPointsUrl,
    success: function(trackPoints) {
        var points = trackPoints['points'];
        for(var index in points){
            if(points.hasOwnProperty(index)){
                var point = points[index];

                var lat = point['lat'];
                var lon = point['lon'];
                var name = point['name'];

                var marker1 = L.marker([lat, lon]).addTo(map);
                marker1.bindPopup(name);
            }
        }
    }
});

На карте отображаются маршрутные точки.

Live Demo: http://demo.antonshell.me/leaflet_examples/example_08.html

Итоги

В результате у нас получилась карта на которой отображается gpx трек, высоты для него и маршрутные точки.

В следующей статье рассмотрим конкатенацию gpx, подключение множественных треков, отображение марщрутных точек и оптимизацию производительности. Код примеров доступен на github: https://github.com/antonshell/leaflet_examples На этом пока все. Спасибо за внимание!