Построение маршрута на OpenLayers 3

Если вы программист, работающий в сфере ГИС, то велика вероятность однажды встретить такую задачу, как построение маршрута. Снова упомяну, что OpenLayers 3 — это только библиотека для работы с картой на клиентской стороне, с ней в комплекте не идет сервис построения маршрута. А значит, все нужно сделать самим.


Маршрут Конечно, не совсем все придется делать самим… Нам не понадобится наполнять базу данных адресов, строить графы дорог, писать на их основе логику поиска маршрутов а так же разрабатывать операционную систему — готовых сервисов хватает. Нужно лишь отправить запрос с начальной и конечной точками, прочитать ответ и отобразить на карте результат. Я воспользовался одним из сервисов на основе данных OSM под названием OSMR — Open Source Routing Machine. Кроме того, нам понадобится сервис определения адреса по координатам (обратное геокодирование), чтобы начальная и конечная точки были не малоинформативными Точка1 и Точка2, а конкретными адресами. Итак, начнем.

Ранее мы рассматривали, как сделать свой контрол, воспользуемся этим материалом снова. Только на этот раз не будем создавать все HTML-элементы через createElement, а определим все в разметке, так проще и быстрее:


    <div id="routeControl" class="controlRoute">
        <input type="search" id="startAddress" placeholder="Введите адрес или укажите точку на карте"></input>
        <input type="search" id="finishAddress" placeholder="Введите адрес или укажите точку на карте"></input>
    </div>

Далее идет написание кода контрола по уже известному сценарию. Для построения маршрута нам нужно отслеживать нажатие на поля ввода, чтобы при клике на карте знать, начальную или конечную точку мы устанавливаем, а так же необходимо подписаться на событие клика на карте для непосредственной установки точки:


RouteControl.prototype._init = function () {
    // запоминаем ссылки на HTML-элементы, с которыми в дальнейшем предстоит работать
    this._element = document.getElementById( 'routeControl' );
    this._startAddressInput = document.getElementById( 'startAddress' );
    this._finishAddressInput = document.getElementById( 'finishAddress' );

    // при клике на поле ввода начального адреса на карте будет устанавливаться 
    // точка отправления, при клике на поле конечного адреса - точка прибытия
    this._startAddressInput.addEventListener( 'click', this._setRoutePoint.bind( this, 0 ), false );
    this._finishAddressInput.addEventListener( 'click', this._setRoutePoint.bind( this, 1 ), false );
    
    // подписываемся на событие клика на карте для установки маркеров
    this._map.on('click', this._processClick.bind( this ) );

Еще нужно создать слой, в котором будем отрисовывать маркеры. В OpenLayers 3 маркер — это точка с настроенным стилем, где указывается иконка:


    var iconStyle = new ol.style.Style({
        image : new ol.style.Icon(({
            anchor : [ 0.5, 1 ],
            src : 'markerStart.png'
        }))
    });
    
    this._source = new ol.source.Vector();
    var markerLayer = new ol.layer.Vector({
        source : this._source,
        style : iconStyle
    });
    this._map.addLayer( markerLayer );
};

Стиль iconStyle задает иконку markerStart.png для каждой точки слоя. Иконка будет находится над точкой и выровнена по горизонтали — за это отвечает anchor : [0.5, 1]. Если задать anchor : [0.5, 0.5], точка будет находится прямо в центре иконки, anchor : [0, 0] — точка в левом верхнем углу иконки. Проблема в том, что у всех маркеров будет одна и та же иконка markerStart.png, а нам нужно, чтобы иконки отличались для начальной и конечной точки. Для достижения этой цели мы используем не статический стиль, а функцию генерации стиля:


var styleFunc = function( feature, resolution ) {
    // получаем путь к иконке из объекта слоя
    var marker = feature.get( 'marker' );
    
    return [new ol.style.Style({
        image : new ol.style.Icon({
            anchor : [ 0.5, 1 ],
            src : marker
        })
    })];
};

Данный механизм даёт нам возможность устанавливать разные стили в зависимости от текущего объекта и разрешения (приближения). Это дает массу возможностей — например, можно отображать подписи только на большом приближении за счет параметра resolution, чтобы они не смешивались в кашу при отдалении. Или можно задать разный цвет заливки для полигонов в зависимости от атрибута. В нашем случае мы будем брать путь к маркеру из атрибутов объекта — то есть каждый объект будет сам указывать, с помощью какой иконки он будет отображен на карте. Функция styleFunc передается в конструктор слоя вместо iconStyle.

Все готово к установке маркеров на карту — слой создан, стиль подготовлен. Осталось написать код для добавления маркера при клике на карте в функции _processClick:


RouteControl.prototype._createPoint = function ( coords, pathToMarker ) {
    return new ol.Feature({
        marker: pathToMarker,
        geometry: new ol.geom.Point(coords)
    });
};

RouteControl.prototype._processClick = function ( evt ) {
    if ( (this._numPoint == null || this._numPoint === 0) && !this._startPoint ) {
        this._startPoint = this._createPoint( evt.coordinate, 'markerStart.png' );
        this._source.addFeatures( [this._startPoint] );
    } else if ( (this._numPoint == null || this._numPoint === 1) && !this._finishPoint ) {
        this._finishPoint = this._createPoint( evt.coordinate, 'markerFinish.png' );
        this._source.addFeatures( [this._finishPoint] );
    }
    this._numPoint = null;
};

Условие довольно сложное, но это лишь для того, чтобы не принуждать пользователя кликать в поля ввода для установки маркера. Если при открытии просто нажать два раза на каре, то установится точка отправления, а затем точка назначения. Можно установить фокус на поле ввода для точки назначения и указать сначала ее, а следующим кликом установится незаполненная точка отправления. Сам маркер создается в функции _createPoint, где указываются координаты точки и иконка маркера.

Следующим шагом будет заполнение адресов в полях ввода и построение маршрута. Заполнение адреса происходит при установке маркера:


RouteControl.prototype._processClick = function ( evt ) {
    if ( (this._numPoint == null || this._numPoint === 0) && !this._startPoint ) {
        this._startPoint = this._createPoint( evt.coordinate, 'markerStart.png' );
        this._source.addFeatures( [this._startPoint] );
        this._fillAddress( evt.coordinate, this._startAddressInput );
    } else if ( (this._numPoint == null || this._numPoint === 1) && !this._finishPoint ) {
        this._finishPoint = this._createPoint( evt.coordinate, 'markerFinish.png' );
        this._source.addFeatures( [this._finishPoint] );
        this._fillAddress( evt.coordinate, this._finishAddressInput );
    }
    this._numPoint = null;
};

Функция fillAddress принимает два параметра — координаты, по которым нужно определить адрес, и поле ввода, которое нужно заполнить ответом от сервера. В качестве сервиса определения адреса я взял nominatim.openstreetmap.org — тот же, что и на OSMR. В запросе отправляется широта и долгота, а возвращается JSON, в поле display_name которого приходит полный адрес:


RouteControl.prototype._fillAddress = function ( coords, input ) {
    var coordsWgs = ol.proj.transform( coords, 'EPSG:3857', 'EPSG:4326' );
    var url = 'http://nominatim.openstreetmap.org/reverse?format=json&lon='
        + coordsWgs[0] + '&lat=' + coordsWgs[1];
    var request = new XMLHttpRequest();
    request.open( "GET", url );
    request.onreadystatechange = function () {
        if (request.readyState == 4) {
            var address = JSON.parse( request.responseText );
            input.value = address.display_name;
        }
    }
    request.send();
};

И, наконец, само построение маршрута. На сайте OSRM построение маршрута снабжается массой информации — вплоть до каждого поворота и длины каждого отрезка пути. Нам не нужно столько деталей, а нужна лишь суммарная информация о маршруте. К счастью, в сервисе присутствует масса настроек для управления выводом информации, которые описаны в документации API. Нужно лишь установить параметр steps=false, чтобы отключить подробный вывод, и добавить параметр overview=true, чтобы сервис возвращал обобщенную информацию. Стоит еще отметить, что сервис возвращает геометрию в компактном формате закодированной полилинии (при желании можно настроить ответ на GeoJSON с помощью параметра geometries=geojson), будем использовать для декодирования соответствующий формат OL3 — ol.format.Polyline


RouteControl.prototype._parseRoute = function ( routesInfo ) {
    var routes = routesInfo.routes;
    if ( routes.length > 0 ) {
        var route = routes[0];
        // преобразуем геометрию Polyline в формат OpenLayers 3
        var polyGeom = route.geometry;
        var polylineFormat = new ol.format.Polyline();
        var olFeature = polylineFormat.readFeature( polyGeom );
        olFeature.getGeometry().transform( "EPSG:4326", "EPSG:3857" );
        this._source.addFeatures( [olFeature] );
    }
};

RouteControl.prototype._buildRoute = function () {
    if ( !this._route ) {
        // получаем координаты точки отправления и прибытия в WGS84
        var startCoords = this._startPoint.getGeometry().getCoordinates();
        var finishCoords = this._finishPoint.getGeometry().getCoordinates();
        var startWgs = ol.proj.transform( startCoords, 'EPSG:3857', 'EPSG:4326' );
        var finishWgs = ol.proj.transform( finishCoords, 'EPSG:3857', 'EPSG:4326' );
        
        // формируем запрос на получение маршрута
        var routeUrl = 'https://router.project-osrm.org/route/v1/driving/'
            + startWgs[0] + ',' + startWgs[1] + ';'+ finishWgs[0] + ',' + finishWgs[1]
            + '?alternatives=false&steps=false&hints=;&overview=full';
        
        var self = this;
        var request = new XMLHttpRequest();
        request.open( "GET", routeUrl );
        request.onreadystatechange = function () {
            if (request.readyState == 4) {
                var routes = JSON.parse( request.responseText );
                self._parseRoute( routes );
            }
        }
        request.send();
    }
};

Готово! Мы получили относительно несложное построение маршрута. «Несложное», потому что нельзя двигать точки отправления и прибытия на карте, нельзя добавлять промежуточные точки, нет подсказок над маркерами и прочих удобств. Но статья итак получилась довольно объемной и нагружать ее сверх меры я не стал. Тем более получилось довольно законченное ядро, отталкиваясь от которого можно сделать своё построение маршрута и совершенствовать его до изнеможения 🙂 .




Ссылки:

Построение маршрута на OpenLayers 3
Метки:    

4 thoughts on “Построение маршрута на OpenLayers 3

  • 28.09.2016 at 08:52
    Permalink

    Добрый день, спасибо за ваши статьи! Не подскажите каким образом построить маршрут через три точки и более?

    Ответить
    • 28.09.2016 at 21:24
      Permalink

      Рад, что материал оказался полезным! Промежуточные точки (waypoints) можно добавить между startWgs и finishWgs в функции _buildRoute. Для двух промежуточных точек выглядеть будет примерно так:

      // формируем запрос на получение маршрута
      var routeUrl = ‘https://router.project-osrm.org/route/v1/driving/’
      + startWgs[0] + ‘,’ + startWgs[1] + ‘;’+ waypoint1[0] + ‘,’ + waypoint1[1] + ‘;’
      + waypoint2[0] + ‘,’ + waypoint2[1] + ‘;’+ finishWgs[0] + ‘,’ + finishWgs[1]
      + ‘?alternatives=false&steps=false&hints=;&overview=full’;

      Ответить
  • 14.02.2017 at 08:26
    Permalink

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

    Ответить
    • 18.02.2017 at 23:16
      Permalink

      Была проблема в стилях векторного слоя. Для новой версии OL задал отдельно стиль для точки и стиль для линии. Теперь в последней версии (4.0.1) пример работает.

      Ответить

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *