Альтернативные маршруты

В системах веб-картографии альтернативные маршруты формируются на основе одного запроса к маршрутизирующему сервису, возвращающему несколько вариантов пути между двумя или более точками. Эти варианты различаются по критериям: расстояние, время, тип дорог, ограничения движения, профиль высот.

Типовая схема работы включает три уровня:

  • Клиентская визуализация (OpenLayers) — отображение и взаимодействие с маршрутами
  • Маршрутизирующий сервис (OSRM, GraphHopper, Mapbox Directions API) — вычисление альтернатив
  • Геометрическое представление (GeoJSON / polyline) — структура маршрутов для отрисовки

OpenLayers не выполняет маршрутизацию самостоятельно, но предоставляет гибкую систему отображения и управления несколькими геометриями.


Структура ответа маршрутизирующего сервиса

Альтернативные маршруты обычно возвращаются в виде массива объектов, каждый из которых содержит:

  • геометрию маршрута (LineString или encoded polyline)
  • метаданные (время, расстояние, теги)
  • сегменты (legs) с детализацией поворотов

Пример упрощённой структуры 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.


Базовая модель отображения альтернативных маршрутов в 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);
  });
}

Использование WebGL-рендеринга

Для высокой нагрузки применяется WebGLVectorLayer:

  • ускоренная отрисовка линий
  • поддержка десятков тысяч сегментов
  • снижение нагрузки на CPU

Структура остаётся аналогичной, меняется только слой визуализации.


Геометрические преобразования маршрутов

Дополнительные операции над маршрутами:

  • сглаживание линий
  • интерполяция точек
  • пересчёт в другую проекцию
feature.getGeometry().transform('EPSG:4326', 'EPSG:3857');

Приоритизация маршрутов

Алгоритмически маршруты сортируются по весу:

  • минимальное время
  • минимальная дистанция
  • минимальная стоимость
data.routes.sort((a, b) => a.duration - b.duration);

Первый маршрут часто назначается дефолтным активным вариантом.


Взаимодействие маршрутов с другими слоями карты

Альтернативные маршруты часто пересекаются с:

  • слоями пробок
  • POI-объектами
  • зонами ограничений

Для корректного отображения используется z-index:

routeLayer.setZIndex(10);

Обработка пересечений и конфликтов отображения

При плотной геометрии маршрутов возникает визуальное слияние линий. Для решения:

  • смещение линий (offset)
  • увеличение прозрачности неактивных маршрутов
  • разделение по высоте символического слоя

Использование событий наведения

Подсветка маршрута при наведении:

map.on('pointermove', function (evt) {
  map.forEachFeatureAtPixel(evt.pixel, function (feature) {
    feature.set('hover', true);
  });
});

В стиле это отражается увеличением ширины линии.


Связь альтернативных маршрутов с UI-логикой

Хотя визуализация выполняется в OpenLayers, логика выбора маршрута часто синхронизируется с внешними интерфейсами:

  • список маршрутов
  • карточки вариантов
  • индикаторы времени

Каждый feature связывается с UI через идентификатор routeIndex или routeId.