В системах веб-картографии альтернативные маршруты формируются на основе одного запроса к маршрутизирующему сервису, возвращающему несколько вариантов пути между двумя или более точками. Эти варианты различаются по критериям: расстояние, время, тип дорог, ограничения движения, профиль высот.
Типовая схема работы включает три уровня:
OpenLayers не выполняет маршрутизацию самостоятельно, но предоставляет гибкую систему отображения и управления несколькими геометриями.
Альтернативные маршруты обычно возвращаются в виде массива объектов, каждый из которых содержит:
Пример упрощённой структуры GeoJSON:
{
"routes": [
{
"distance": 12450,
"duration": 1350,
"geometry": {
"type": "LineString",
"coordinates": [[30.31, 59.93], [30.33, 59.94]]
}
},
{
"distance": 13200,
"duration": 1280,
"geometry": {
"type": "LineString",
"coordinates": [[30.31, 59.93], [30.35, 59.95]]
}
}
]
}
Каждый элемент массива становится отдельным визуальным слоем или отдельной feature в OpenLayers.
Основная задача — преобразовать набор маршрутов в набор объектов
ol.Feature и отобразить их через единый
VectorLayer.
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import VectorSource from 'ol/source/Vector.js';
import VectorLayer from 'ol/layer/Vector.js';
import GeoJSON from 'ol/format/GeoJSON.js';
import Stroke from 'ol/style/Stroke.js';
import Style from 'ol/style/Style.js';
Создание источника данных:
const routeSource = new VectorSource();
Создание слоя:
const routeLayer = new VectorLayer({
source: routeSource,
style: function (feature) {
const isActive = feature.get('active');
return new Style({
stroke: new Stroke({
color: isActive ? '#1e88e5' : '#90caf9',
width: isActive ? 5 : 3
})
});
}
});
Карта:
const map = new Map({
target: 'map',
layers: [routeLayer],
view: new View({
center: [0, 0],
zoom: 4
})
});
Получение данных от API и добавление их в источник:
fetch('/api/routes?from=pointA&to=pointB')
.then(res => res.json())
.then(data => {
const format = new GeoJSON();
data.routes.forEach((route, index) => {
const feature = format.readFeature(route.geometry, {
featureProjection: 'EPSG:3857'
});
feature.setProperties({
routeIndex: index,
distance: route.distance,
duration: route.duration,
active: index === 0
});
routeSource.addFeature(feature);
});
});
Каждый маршрут становится отдельным Feature, что
позволяет управлять ими независимо.
Альтернативные маршруты предполагают переключение активного варианта. Это реализуется через изменение свойств feature.
function setActiveRoute(index) {
routeSource.getFeatures().forEach(f => {
f.set('active', f.get('routeIndex') === index);
});
routeLayer.changed();
}
Изменение свойства active влияет на стиль отображения,
выделяя выбранный маршрут.
При большом количестве альтернатив используется стратегия разделения:
const baseLayer = new VectorLayer({
source: routeSource,
style: new Style({
stroke: new Stroke({
color: '#b0bec5',
width: 3
})
})
});
const activeLayer = new VectorLayer({
source: new VectorSource()
});
При переключении активного маршрута геометрия копируется в отдельный слой:
function promoteActiveRoute(index) {
const feature = routeSource.getFeatures()
.find(f => f.get('routeIndex') === index);
activeLayer.getSource().clear();
activeLayer.getSource().addFeature(feature.clone());
}
Взаимодействие с альтернативами реализуется через
forEachFeatureAtPixel.
map.on('click', function (evt) {
map.forEachFeatureAtPixel(evt.pixel, function (feature) {
const index = feature.get('routeIndex');
setActiveRoute(index);
});
});
Для повышения точности можно использовать приоритет слоёв и фильтрацию по типу feature.
Разные маршруты должны быть различимы даже без взаимодействия. Используются:
new Style({
stroke: new Stroke({
color: 'rgba(33, 150, 243, 0.6)',
width: 4,
lineDash: [8, 6]
})
});
Активный маршрут обычно выделяется сплошной линией и увеличенной толщиной.
При большом количестве маршрутов (10–50 вариантов) возникает нагрузка на рендеринг WebGL/canvas.
Применяются следующие техники:
Сегментация и упрощение линий:
import simplify from 'simplify-js';
или серверная генерализация через API маршрутизатора.
Отображаются только первые N маршрутов:
const visibleRoutes = data.routes.slice(0, 5);
const routeCache = new Map();
function getCachedRoute(id, geometry) {
if (!routeCache.has(id)) {
routeCache.set(id, geometry);
}
return routeCache.get(id);
}
Для анализа альтернатив используются вычисляемые метрики:
Пример расчёта:
function compareRoutes(base, alternative) {
return {
timeDiff: alternative.duration - base.duration,
distanceDiff: alternative.distance - base.distance
};
}
Результаты могут использоваться для визуальных меток прямо на карте.
Каждый маршрут может иметь подпись с ключевыми параметрами.
import Text from 'ol/style/Text.js';
new Style({
text: new Text({
text: '12.4 км / 18 мин',
offsetY: -10
})
});
Подписи часто размещаются в средней точке линии, вычисляемой через
геометрию LineString.
Некоторые API возвращают детализированные сегменты (legs), которые можно визуализировать отдельно:
Каждый сегмент становится отдельной feature с уникальным стилем.
feature.set('traffic', 'heavy');
stroke: new Stroke({
color: feature.get('traffic') === 'heavy' ? 'red' : 'green'
})
При изменении начальной или конечной точки маршруты пересчитываются и заменяются:
function updateRoutes(newData) {
routeSource.clear();
newData.routes.forEach((route, i) => {
const feature = new GeoJSON().readFeature(route.geometry, {
featureProjection: 'EPSG:3857'
});
feature.set('routeIndex', i);
routeSource.addFeature(feature);
});
}
Для высокой нагрузки применяется WebGLVectorLayer:
Структура остаётся аналогичной, меняется только слой визуализации.
Дополнительные операции над маршрутами:
feature.getGeometry().transform('EPSG:4326', 'EPSG:3857');
Алгоритмически маршруты сортируются по весу:
data.routes.sort((a, b) => a.duration - b.duration);
Первый маршрут часто назначается дефолтным активным вариантом.
Альтернативные маршруты часто пересекаются с:
Для корректного отображения используется z-index:
routeLayer.setZIndex(10);
При плотной геометрии маршрутов возникает визуальное слияние линий. Для решения:
Подсветка маршрута при наведении:
map.on('pointermove', function (evt) {
map.forEachFeatureAtPixel(evt.pixel, function (feature) {
feature.set('hover', true);
});
});
В стиле это отражается увеличением ширины линии.
Хотя визуализация выполняется в OpenLayers, логика выбора маршрута часто синхронизируется с внешними интерфейсами:
Каждый feature связывается с UI через идентификатор
routeIndex или routeId.