Если вы программист, работающий в сфере ГИС, то велика вероятность однажды встретить такую задачу, как построение маршрута. Снова упомяну, что 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();
}
};
Готово! Мы получили относительно несложное построение маршрута. «Несложное», потому что нельзя двигать точки отправления и прибытия на карте, нельзя добавлять промежуточные точки, нет подсказок над маркерами и прочих удобств. Но статья итак получилась довольно объемной и нагружать ее сверх меры я не стал. Тем более получилось довольно законченное ядро, отталкиваясь от которого можно сделать своё построение маршрута и совершенствовать его до изнеможения 🙂 .
Ссылки:
Добрый день, спасибо за ваши статьи! Не подскажите каким образом построить маршрут через три точки и более?
Рад, что материал оказался полезным! Промежуточные точки (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’;
Похоже, что метод больше не поддерживается. Необходимо искать альтернативу.
Была проблема в стилях векторного слоя. Для новой версии OL задал отдельно стиль для точки и стиль для линии. Теперь в последней версии (4.0.1) пример работает.
Добрый день. Подскажите, пожалуйста, как решить похожую задачу: На страничке есть OSM-карта и форма, на которой помимо прочих параметров есть поля ввода координат точки А и точки B. При вводе координат на карте должны проставляться маркеры этих двух точек, и наоборот — при клике на карту точки должны перемещаться и меняться их координаты в полях ввода. При нажатии на Submit эти координаты должны отправляться Get-запросом. Ответ можно по почте.
Этот пример будет ещё проще, так как не нужно будет получать координаты по адресу — координаты сразу будут предоставляться пользователем. Добавление маркера будет происходить точно так же — через функцию addFeatures. Отправка запроса — через XMLHttpRequest. Пример нужно лишь слегка изменить.
Пожалуйста помогите разобраться. RouteControl.prototype._fillAddress и RouteControl.prototype._parseRoute я так понимаю, мне не нужен. Мне нужно получать координаты точек в окошках и желательно соединить MarkerStart и MarkerFinish прямой.
Если нужно соединить точки прямой линией, то функции _fillAddress и _parseRoute не нужны. Достаточно просто создать линию ol.geom.LineString, куда передать две точки, а затем добавить эту геометрию на карту.
Добрый день, Сергей. Не могли бы Вы помочь со следующей задачей: 1. Отображаем на карте массив иконок по заданным координатам из .kml-файла. Это работает так:
//показать kml файл
var vector = new ol.layer.Vector({
source: new ol.source.Vector({
url: ‘test.kml’,
format: new ol.format.KML({showPointNames: true})
}),
visible: true
});
//showPointNames: true — белые name около иконок
map.addLayer(vector);
2. Каждая иконка должна быть кликабельна и при нажатии на нее должен быть переход по адресу, указанному для каждой точки в kml-файле. Вот с этим у меня затык…
Добрый день!
KML — это обычный векторный слой, поэтому обработка клика будет такая же, как и на других векторных слоях:
1. Вешаем событие на карту map.on(‘click’, function …
2. Находим объекты, в которые мы попали (нужно проверить, что это наш слой и мы нашли один объект)
3. Получаем из найденного объекта атрибут с адресом и выполняем переход
Посмотреть примеры с событием можно здесь:
https://stackoverflow.com/questions/43004561/how-do-i-add-a-click-event-on-openlayers-4
https://gis.stackexchange.com/questions/126581/openlayers-3-capturing-various-click-events
Если будут сложности — присылайте KML, попробую выкроить время для живого примера
Сергей, тяжело идет. Куда можно положить тестовый KML?
Ответил на почту
Сергей, подскажите пожалуйста, как получить массив координат построенного маршрута?
Комментарий затерялся в спаме, поэтому отвечаю только сейчас (.
В функции _parseRoute создаётся olFeature, и если вызвать у неё getGeometry(), то получим геометрию построенного маршрута, который добавляется на карту. Массив координат у этой линии можно получить через getCoordinates()
У меня одного маршрут не прокладывается? Пишет ошибку «Запрос из постороннего источника заблокирован: Политика одного источника запрещает чтение удаленного ресурса на https://router.project-osrm.org/route/v1/driving/37.61959956…3874194?alternatives=false&steps=false&hints=;&overview=full. (Причина: не удалось выполнить запрос CORS).»
Нет, у меня сейчас тоже не работает. Виной тому — сертификат на сайте https://router.project-osrm.org. Если в отдельной вкладке зайти на этот сайт и принять сертификат (в разных браузерах кнопка звучит по разному, но обычно рядом с ней пишут Небезопасно), то маршрут начнёт строиться. Скорей всего это временна проблема, 9 марта истёк срок действия сертификата и вскоре это должны заметить и продлить сертификат.
Большое спасибо за ответ, очень приятно его получить!
Также хотелось бы спросить. Если в итоге не починят, есть ли возможность использования какого-нибудь другого сервиса построения маршрута? Собственно, думаю да, подскажите какие?
Вот подборка со сравнением функционала на основе OSM — https://wiki.openstreetmap.org/wiki/Routing/online_routers
Работаю с OL 6, браузер говорит, что RouteControl не определен.
Еще не совсем понимаю вы используете здесь только ol , osmr и все?
RouteControl — это не стандартный контрол, он определяется прямо в коде HTML-страницы. Используется только OL и OSMR, код можно глянуть в GitHub — https://github.com/kolosov-sergey/webgl_blog/tree/master/ol/Route