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

Маршрутизация в веб-картах на базе MapLibre GL JS строится вокруг связки внешнего routing-сервиса и визуализации геометрии маршрута в виде GeoJSON-слоя. Библиотека не предоставляет собственного движка построения маршрутов, поэтому вся логика расчёта пути выносится в специализированные API, а MapLibre GL JS отвечает за отображение, интерактивность и обновление данных на карте.


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

  • UI-слой: выбор точек отправления и назначения
  • Routing API: вычисление маршрута (OSRM, GraphHopper, Mapbox Directions API и др.)
  • Map rendering: отображение результата в MapLibre GL JS через GeoJSON source

Ключевой принцип — маршрут всегда приходит как геометрия (LineString или encoded polyline), которая преобразуется в GeoJSON.


Подключение источника маршрута в MapLibre GL JS

Для отображения маршрута используется динамический GeoJSON source:

map.on('load', () => {
  map.addSource('route', {
    type: 'geojson',
    data: {
      type: 'Feature',
      geometry: {
        type: 'LineString',
        coordinates: []
      }
    }
  });

  map.addLayer({
    id: 'route-line',
    type: 'line',
    source: 'route',
    paint: {
      'line-color': '#3b82f6',
      'line-width': 5,
      'line-opacity': 0.85
    }
  });
});

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


Получение маршрута через OSRM

Одним из самых распространённых решений является OSRM (Open Source Routing Machine).

Запрос формируется по координатам:

async function fetchRoute(start, end) {
  const query = `https://router.project-osrm.org/route/v1/driving/` +
                `${start[0]},${start[1]};${end[0]},${end[1]}` +
                `?overview=full&geometries=geojson`;

  const response = await fetch(query);
  const data = await response.json();

  return data.routes[0].geometry;
}

Ключевые параметры:

  • overview=full — полная геометрия маршрута
  • geometries=geojson — возврат в формате GeoJSON вместо polyline

Обновление маршрута на карте

После получения геометрии выполняется обновление источника:

async function updateRoute(start, end) {
  const geometry = await fetchRoute(start, end);

  map.getSource('route').setData({
    type: 'Feature',
    geometry: geometry
  });
}

Обновление через setData позволяет мгновенно перерисовывать линию без пересоздания слоя.


Работа с альтернативными маршрутами

Некоторые API возвращают несколько вариантов пути. Структура обычно содержит массив routes.

const routeGeo = data.routes.map(r => ({
  geometry: r.geometry,
  distance: r.distance,
  duration: r.duration
}));

Отображение альтернатив реализуется через несколько источников:

map.addSource('route-alt', {
  type: 'geojson',
  data: { type: 'FeatureCollection', features: [] }
});

Каждый маршрут может быть оформлен разной прозрачностью:

  • основной маршрут — яркий цвет
  • альтернативные — серые, менее заметные линии

Интерактивное построение маршрута

Часто используется сценарий выбора точек кликом по карте:

let start = null;
let end = null;

map.on('click', (e) => {
  if (!start) {
    start = [e.lngLat.lng, e.lngLat.lat];
  } else {
    end = [e.lngLat.lng, e.lngLat.lat];
    updateRoute(start, end);
    start = null;
    end = null;
  }
});

Такая модель применяется в минималистичных навигационных интерфейсах.


Перетаскиваемые точки маршрута

Для более сложного UX используется draggable markers:

const markerStart = new maplibregl.Marker({ draggable: true })
  .setLngLat([30.3, 59.9])
  .addTo(map);

const markerEnd = new maplibregl.Marker({ draggable: true })
  .setLngLat([30.4, 59.8])
  .addTo(map);

function refresh() {
  const start = markerStart.getLngLat();
  const end = markerEnd.getLngLat();
  updateRoute([start.lng, start.lat], [end.lng, end.lat]);
}

markerStart.on('dragend', refresh);
markerEnd.on('dragend', refresh);

Пересчёт маршрута происходит при каждом завершении drag-события.


Декодирование polyline (если API не возвращает GeoJSON)

Некоторые сервисы используют encoded polyline. В этом случае требуется декодирование:

function decodePolyline(str) {
  let index = 0;
  let lat = 0;
  let lng = 0;
  const coordinates = [];

  while (index < str.length) {
    let b, shift = 0, result = 0;

    do {
      b = str.charCodeAt(index++) - 63;
      result |= (b & 0x1f) << shift;
      shift += 5;
    } while (b >= 0x20);

    const dlat = ((result & 1) ? ~(result >> 1) : (result >> 1));
    lat += dlat;

    shift = 0;
    result = 0;

    do {
      b = str.charCodeAt(index++) - 63;
      result |= (b & 0x1f) << shift;
      shift += 5;
    } while (b >= 0x20);

    const dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
    lng += dlng;

    coordinates.push([lng / 1e5, lat / 1e5]);
  }

  return coordinates;
}

После декодирования данные преобразуются в GeoJSON LineString.


Стилизация маршрута

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

map.addLayer({
  id: 'route-line',
  type: 'line',
  source: 'route',
  paint: {
    'line-color': [
      'interpolate',
      ['linear'],
      ['line-progress'],
      0, '#22c55e',
      1, '#3b82f6'
    ],
    'line-width': 6,
    'line-blur': 0.5
  }
});

Возможные приёмы:

  • градиент по прогрессу маршрута
  • анимация «движения» по линии через line-dasharray
  • разделение на сегменты (город / трасса)

Обновление маршрута без мерцания

Чтобы избежать визуальных артефактов:

  • используется один источник данных
  • применяется setData, а не удаление слоя
  • предварительно кешируется последняя геометрия
let cachedRoute = null;

function safeUpdate(geometry) {
  if (JSON.stringify(cachedRoute) === JSON.stringify(geometry)) return;
  cachedRoute = geometry;

  map.getSource('route').setData({
    type: 'Feature',
    geometry
  });
}

Учёт шагов маршрута (turn-by-turn)

Routing API часто возвращают legs и steps:

const steps = data.routes[0].legs[0].steps;

Каждый шаг содержит:

  • манёвр (turn left, right)
  • координаты начала
  • расстояние
  • инструкцию

Эти данные используются для построения UI навигации и синхронизации с позицией на карте.


Привязка маршрута к движению объекта

Для анимации движения по маршруту применяется интерполяция координат:

function animateAlongRoute(coords, progress) {
  const index = Math.floor(progress * (coords.length - 1));
  return coords[index];
}

Marker обновляется через requestAnimationFrame, создавая эффект движения по линии.


Оптимизация частых пересчётов

При интерактивных сценариях (drag, live routing) важны ограничения:

  • debounce запросов к API
  • ограничение частоты пересчёта (300–500 мс)
  • кеширование маршрутов для одинаковых точек
  • использование Web Worker для декодирования polyline

Обработка ошибок маршрутизации

Типовые случаи:

  • отсутствие маршрута между точками
  • неверные координаты
  • перегрузка routing API

Обработка строится через fallback:

if (!data.routes || data.routes.length === 0) {
  map.getSource('route').setData({
    type: 'Feature',
    geometry: { type: 'LineString', coordinates: [] }
  });
}

Сочетание с 3D-слоями и terrain

В MapLibre GL JS маршруты часто визуализируются поверх 3D-ландшафта:

  • fill-extrusion для зданий
  • terrain DEM для рельефа
  • поднятие линии маршрута над поверхностью
paint: {
  'line-translate': [0, 0],
  'line-z-offset': 1
}

Это предотвращает «проваливание» линии в поверхность карты.


Сегментация маршрута по типам дорог

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

  • автомагистраль
  • городская дорога
  • грунтовая дорога

Используется feature-state или разделение LineString на сегменты с разными стилями.


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

Автоматическое центрирование:

const bounds = new maplibregl.LngLatBounds();

coords.forEach(c => bounds.extend(c));

map.fitBounds(bounds, {
  padding: 50
});

Это обеспечивает корректное отображение всего маршрута на экране.